/*
 * 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.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;

import org.xml.sax.Attributes;
import org.xml.sax.SAXException;
import org.xml.sax.helpers.DefaultHandler;

import jp.sourceforge.mergedoc.pleiades.log.Logger;
import jp.sourceforge.mergedoc.pleiades.runtime.advice.JointPoint.EditPoint;

/**
 * Pleiades 構成ファイルの内容を取得・構築するアセンブラです。<br>
 * ジョイント・ポイントとポイント・カットの関連付けを行います。
 * <p>
 * @author cypher256
 */
public class PleiadesConfigAssembler extends DefaultHandler {

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

	/** プロパティーを保持するマップ */
	protected final Map<String, String> propertyMap = new HashMap<String, String>();

	/** AOP 除外パッケージ・セット */
	protected final Set<String> excludePackageSet = new HashSet<String>();

	/** ジョイント・ポイントとポイント・カットを保持するマップ */
	protected final Map<JointPoint, PointCut> jointMap = new HashMap<JointPoint, PointCut>();

	/** ポイント・カット */
	private PointCut pointCut;

	/** 要素 body 文字列バッファ */
	private StringBuilder elementBodyBuffer;

	/** ジョイント・ポイント */
	private JointPoint jointPoint;

	/** 編集ポイント */
	private String editPoint;

	/**
	 * XML 要素開始時の処理です。
	 */
	@Override
	public void startElement(String uri, String localName, String qName,
			Attributes attributes) throws SAXException {

		if (qName.equals("pleiades")) {

		} else if (qName.equals("property")) {

			String name = attributes.getValue("name");
			String value = replacePropertyVariables(attributes.getValue("value"));
			propertyMap.put(name, value);

		} else if (qName.equals("pointCut")) {

			pointCut = new PointCut();
			pointCut.setTiming(attributes.getValue("timing"));
			pointCut.setLevel(attributes.getValue("level"));
			editPoint = attributes.getValue("editPoint");

		} else if (qName.equals("advice") || qName.equals("excludePackage")) {

			elementBodyBuffer = new StringBuilder();

		} else if (qName.equals("jointPoint")) {

			jointPoint = createJointPoint(new JointPoint(), attributes);

			/* TranslationEditor のコンストラクタ対応メソッド実装済み 2017.02.08
			String methodName = jointPoint.getMethodName();
			if (
				jointPoint.getEditPoint() == EditPoint.CALL &&
				methodName != null && !methodName.equals("") && Character.isUpperCase(methodName.charAt(0))
			) {
				// TranslationEditor のコンストラクタ対応メソッドを実装すれば対応可能
				throw new IllegalStateException(
					"pointCut の editPoint が call の場合、jointPoint にコンストラクタを指定することはできません。" +
					jointPoint);
			}
			*/
			if (jointMap.containsKey(jointPoint)) {
				log.warn("ジョイント・ポイント定義が重複しています。" + jointPoint);
			}
			PointCut curPointCut = new PointCut(pointCut);
			jointMap.put(jointPoint, curPointCut);
			curPointCut.setJointPoint(jointPoint);

		} else if (qName.endsWith(/* include～ or exclude～ */"cludeWhere")) {

			PointCut curPointCut = jointMap.get(jointPoint);
			if (curPointCut.getJointPoint().getEditPoint() == EditPoint.EXECUTION) {
				throw new IllegalStateException(
					"editPoint が execution の場合、*cludeWhere 属性を指定することはできません。" + curPointCut);
			}

			// *cludeWhere の descriptor は未サポート
			JointPoint where = createJointPoint(new JointPoint(), attributes);

			if (qName.equals("excludeWhere")) {
				curPointCut.getExcludeWheres().add(where);
			} else if (qName.equals("includeWhere")) {
				curPointCut.getIncludeWheres().add(where);
			} else {
				throw new IllegalStateException("不正な属性名です。" + qName);
			}
			if (curPointCut.getExcludeWheres().size() > 0 && curPointCut.getIncludeWheres().size() > 0) {
				throw new IllegalStateException(
					"同じ jointPoint 内に excludeWhere と includeWhere を同時に指定できません。" +
					curPointCut);
			}

		} else if (qName.endsWith(/* include～ or exclude～ */"cludeTrace")) {

			PointCut curPointCut = jointMap.get(jointPoint);
			if (curPointCut.getJointPoint().getEditPoint() == EditPoint.CALL) {
				throw new IllegalStateException(
					"editPoint が call の場合、*cludeTrace 属性を指定することはできません。" +
					curPointCut);

			}
			if (!curPointCut.getAdvice().contains("?{JOINT_POINT}")) {
				throw new IllegalStateException(
					"*cludeTrace を指定している場合は Advice に ?{JOINT_POINT} が" +
					"含まれている必要があります。" + curPointCut);
			}
			// *cludeTrace の descriptor は未サポート
			TraceJointPoint where = new TraceJointPoint();
			String limit = attributes.getValue("limit");
			if (limit != null && !limit.isEmpty()) {
				int lim = Integer.parseInt(limit);
				if (lim <= 0) {
					lim = Integer.MAX_VALUE;
				}
				where.setLimit(lim);
			}
			createJointPoint(where, attributes);

			if (qName.equals("excludeTrace")) {
				curPointCut.getExcludeTrace().add(where);
			} else if (qName.equals("includeTrace")) {
				curPointCut.getIncludeTrace().add(where);
			} else {
				throw new IllegalStateException("不正な属性名です。" + qName);
			}
			/* 両方指定されている場合は include 優先 (CallHierarchyExplorer 参照) 2017.01.18
			if (curPointCut.getExcludeTrace().size() > 0 && curPointCut.getIncludeTrace().size() > 0) {
				throw new IllegalStateException(
					"同じ jointPoint 内に excludeTrace と includeTrace を同時に指定できません。" +
					curPointCut);
			}
			*/

		} else {
			throw new IllegalStateException("不正な属性名です。" + qName);
		}
	}

	/**
	 * 属性から JointPoint を作成します。
	 * <p>
	 * @param jointPoint
	 * @param attributes 属性
	 * @return JointPoint
	 */
	private JointPoint createJointPoint(JointPoint jointPoint, Attributes attributes) {

		jointPoint.setEditPoint(editPoint);
		jointPoint.setClassName(attributes.getValue("className"));
		jointPoint.setMethodName(attributes.getValue("methodName"));

		String descriptor = attributes.getValue("descriptor");
		jointPoint.setDescriptor(descriptor);
		if (descriptor != null && descriptor.matches(".+[a-z]\\).+")) {
			throw new IllegalStateException("descriptor に指定する型の末尾には ; が必要です。" + jointPoint);
		}
		return jointPoint;
	}

	/**
	 * プロパティ変数を置換します。
	 * @param value 値
	 * @return 置換後の値
	 */
	private String replacePropertyVariables(String value) {

		for (Entry<String, String> e : propertyMap.entrySet()) {
			value = value.replaceAll(
				"\\?\\{" + e.getKey() + "\\}",
				e.getValue());
		}
		return value;
	}

	/**
	 * XML 要素終了時の処理です。
	 */
	@Override
	public void endElement(String uri, String localName, String qName)
			throws SAXException {

		if (qName.equals("advice")) {
			String advice = replacePropertyVariables(elementBodyBuffer.toString());
			pointCut.setAdvice(advice);

		} else if (qName.equals("excludePackage")) {
			String excludePackage = elementBodyBuffer.toString().replaceAll("(?s)<!--.*-->", "");
			List<String> list = Arrays.asList(excludePackage.trim().split("\\s+"));
			excludePackageSet.addAll(list);
		}
	}

	/**
	 * XML 要素ボディ部の処理です。
	 */
	@Override
	public void characters(char[] ch, int start, int length) throws SAXException {

		if (elementBodyBuffer != null) {
			elementBodyBuffer.append(ch, start, length);
		}
	}
}
