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

import java.io.File;
import java.io.FileNotFoundException;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;

import javax.xml.parsers.SAXParser;
import javax.xml.parsers.SAXParserFactory;

import jp.sourceforge.mergedoc.org.apache.commons.lang.builder.ToStringBuilder;
import jp.sourceforge.mergedoc.org.apache.commons.lang.builder.ToStringStyle;
import jp.sourceforge.mergedoc.pleiades.Pleiades;
import jp.sourceforge.mergedoc.pleiades.log.Logger;
import jp.sourceforge.mergedoc.pleiades.resource.Files;
import jp.sourceforge.mergedoc.pleiades.runtime.Analyses;
import jp.sourceforge.mergedoc.pleiades.runtime.advice.JointPoint.EditPoint;

/**
 * Pleiades 構成クラスです。<br>
 * ジョイント・ポイントとポイント・カットの関連付けなどを保持します。
 * <p>
 * @author cypher256
 */
public class PleiadesConfig {

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

	/** このクラスのインスタンス */
	private static final PleiadesConfig singleton = new PleiadesConfig();

	/**
	 * Pleiades 構成を取得します。
	 * <p>
	 * @return Pleiades 構成
	 */
	public static PleiadesConfig getInstance() {
		return singleton;
	}

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

	/** Pleiades 構成ファイルの内容を取得・構築するアセンブラ */
    private PleiadesConfigAssembler pleiadesConfigAssembler = new PleiadesConfigAssembler();

	/** call ウィービング対象となるクラス・メソッドセット */
	private Set<String> callNameSet = new HashSet<String>();

	/** call ウィービング対象となるクラス指定なし前方一致セット */
	private Set<String> callClassNameStartsSet = new HashSet<String>();

	/** call ウィービング対象となるクラス指定のみ後方一致セット */
	private Set<String> callClassNameEndsSet = new HashSet<String>();

	/** トレース構成 (通常実行時は null) */
	public final TraceConfig traceConfig;

	/**
	 * アスペクト・マッピングを構築します。
	 */
	private PleiadesConfig() {

		long start = System.nanoTime();

		// JUnit やツールなどからの実行時
		if (Pleiades.getPleiadesOption().configXmlFile == null) {
			log.info("PleiadesOption.configXmlFile が null のため Pleiades 構成ファイルをロードしませんでした。");
			traceConfig = null;
			return;
		}

	    File configFile = Files.conf(Pleiades.getPleiadesOption().configXmlFile);
		try {
		    if (!configFile.exists()) {
		    	throw new FileNotFoundException(configFile.getPath());
		    }
		    SAXParser parser = SAXParserFactory.newInstance().newSAXParser();
		    parser.parse(configFile, pleiadesConfigAssembler);
		    Map<JointPoint, PointCut> jointMap = pleiadesConfigAssembler.jointMap;

		    for (JointPoint jointPoint : jointMap.keySet()) {
				if (jointPoint.getEditPoint() == EditPoint.CALL) {

					String className = jointPoint.getClassName();
					String methodName = jointPoint.getMethodName();

					callNameSet.add(className + "#" + methodName);

					if (methodName == null) {

						if (className.equals(className.toLowerCase())) {
							callClassNameStartsSet.add(className);
						} else if (!className.contains(".")) {
							callClassNameEndsSet.add(className);
						}
					}
				}
			}
			log.info("Pleiades 構成ファイルをロードしました。" + configFile.getName());

		} catch (Exception e) {

			String msg = "Pleiades 構成ファイルのロードに失敗しました。" + configFile;
			log.fatal(msg);
			throw new IllegalStateException(msg, e);
		}

		// トレース構成の作成
		if (log.isDebugEnabled()) {
			traceConfig = new TraceConfig(pleiadesConfigAssembler.propertyMap);
		} else {
			traceConfig = null;
		}

		Analyses.end(PleiadesConfig.class, "<init>", start);
	}

	/**
	 * 呼び出し先クラスとして AOP が定義されているか判定します。
	 * <p>
	 * @param className 呼び出し先クラス名
	 * @param methodName 呼び出し先メソッド名
	 * @return 定義されている場合は true
	 */
	public boolean containsCall(String className, String methodName) {

		long start = System.nanoTime();
		try {

			if (callNameSet.contains(className + "#" + methodName)) {
				return true;
			}
			return getSubstringMatch(className) != null;

		} finally {
			Analyses.end(PleiadesConfig.class, "containsCall", start);
		}
	}

	/**
	 * 部分一致クラス名文字列を取得します。
	 * <p>
	 * @param className クラス名
	 * @return 部分一致クラス名文字列
	 */
	private String getSubstringMatch(String className) {

		long start = System.nanoTime();
		try {

			// クラス名前方一致 (パッケージ名前方部分)
			for (String starts : callClassNameStartsSet) {
				if (className.startsWith(starts)) {
					return starts;
				}
			}
			// クラス名後方一致 (クラス名後方部分)
			for (String ends : callClassNameEndsSet) {
				if (className.startsWith(ends)) {
					return ends;
				}
			}
			return null;

		} finally {
			Analyses.end(PleiadesConfig.class, "getSubstringMatch", start);
		}
	}

	/**
	 * ポイント・カットを取得します。
	 * <p>
	 * @param jointPoint ジョイント・ポイント
	 * @return ポイント・カット
	 */
	public PointCut getPointCut(JointPoint jointPoint) {

	    Map<JointPoint, PointCut> jointMap = pleiadesConfigAssembler.jointMap;
		PointCut pointCut = jointMap.get(jointPoint);
		if (pointCut != null) {
			return pointCut;
		}

		JointPoint key = new JointPoint(jointPoint);
		key.setDescriptor(null);
		pointCut = jointMap.get(key);
		if (pointCut != null) {
			return pointCut;
		}

		key.setMethodName(null);
		pointCut = jointMap.get(key);
		if (pointCut != null) {
			return pointCut;
		}

		if (key.getEditPoint() == EditPoint.CALL) {

			String classNameParts = getSubstringMatch(key.getClassName());
			if (classNameParts != null) {

				key.setClassName(classNameParts);
				pointCut = jointMap.get(key);
				if (pointCut == null) {
					throw new IllegalStateException("ロジック不正。ポイント・カットが見つかりません。");
				}
				return pointCut;
			}
		}
		return null;
	}

	/**
	 * AOP 除外クラスか判定します。
	 * @param className クラス名
	 * @return 除外する場合は true
	 */
	public boolean isExcludePackage(String className) {
		for (String packageName : pleiadesConfigAssembler.excludePackageSet) {
			if (className.startsWith(packageName)) {
				return true;
			}
		}
		return false;
	}

	// TraceConfig で参照するように変更 2017.07.31
	/**
	 * プロパティーを取得します。
	 * <p>
	 * @param name プロパティー名
	 * @return プロパティー
	public String getProperty(String name) {
		return pleiadesConfigAssembler.propertyMap.get(name);
	}
	 */

	/**
	 * このオブジェクトの文字列表現を取得します。
	 */
	public String toString() {

		long start = System.nanoTime();
	    Map<JointPoint, PointCut> jointMap = pleiadesConfigAssembler.jointMap;
		try {
			return new ToStringBuilder(this, ToStringStyle.MULTI_LINE_STYLE)
				.append(jointMap)
				.toString();

		} finally {
			Analyses.end(PleiadesConfig.class, "toString", start);
		}
	}
}
