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

import java.io.IOException;
import java.util.List;
import java.util.regex.Matcher;

import javassist.CannotCompileException;
import javassist.CtBehavior;
import javassist.CtClass;
import javassist.NotFoundException;
import javassist.expr.ConstructorCall;
import javassist.expr.ExprEditor;
import javassist.expr.MethodCall;
import jp.sourceforge.mergedoc.org.apache.commons.lang.StringUtils;
import jp.sourceforge.mergedoc.pleiades.log.Logger;
import jp.sourceforge.mergedoc.pleiades.log.Logger.Level;
import jp.sourceforge.mergedoc.pleiades.runtime.advice.JointPoint;
import jp.sourceforge.mergedoc.pleiades.runtime.advice.JointPoint.EditPoint;
import jp.sourceforge.mergedoc.pleiades.runtime.advice.PleiadesConfig;
import jp.sourceforge.mergedoc.pleiades.runtime.advice.PointCut;
import jp.sourceforge.mergedoc.pleiades.runtime.advice.PointCut.Timing;
import jp.sourceforge.mergedoc.pleiades.runtime.advice.RuntimeJointPoint;
import jp.sourceforge.mergedoc.pleiades.runtime.advice.TraceConfig;

/**
 * 翻訳処理をクラス・オブジェクトに埋め込むエディターです。
 * <p>
 * 対象となるジョイント・ポイントやアドバイスは
 * {@link jp.sourceforge.mergedoc.pleiades.runtime.advice.PleiadesConfig}
 * から取得します。
 * <p>
 * @author cypher256
 */
public class TranslationEditor extends ExprEditor {

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

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

	/** 編集対象となる CtClass オブジェクト */
	private final CtClass ctClass;

	/** 編集済みの場合は true */
	private boolean isEdited;

	/**
	 * 翻訳エディターを構築します。
	 * <p>
	 * @param ctClass CtClass オブジェクト
	 */
	public TranslationEditor(CtClass ctClass) {
		this.ctClass = ctClass;
	}

	/**
	 * コンストラクター呼び出しを編集します。
	 * editPoint が call の場合に呼び出されます。
	 */
	@Override
	public void edit(ConstructorCall call) throws CannotCompileException {
		edit((MethodCall) call);
	}

	/**
	 * メソッド呼び出しを編集します。
	 * editPoint が call の場合に呼び出されます。
	 */
	@Override
	public void edit(MethodCall call) throws CannotCompileException {

		long start = System.nanoTime();
		boolean isConstructor = call instanceof ConstructorCall;

		try {
			//---------------------------------------------------------------------
			// 呼び出し先の情報を取得
			//---------------------------------------------------------------------
			String targetClassName = call.getClassName();
			String targetMethodName = call.getMethodName();
			if (isConstructor) {
				targetMethodName = targetClassName.replaceFirst("(.+?\\.)+", "");
				targetMethodName = optimizeConstructorName(targetClassName, targetMethodName);
			}

			// 後の descriptor 取得はクラスパスなしによる NotFoundException
			// 多発で時間がかかるため、ここで振るい落とし
			PleiadesConfig config = PleiadesConfig.getInstance();
			if (!config.containsCall(targetClassName, targetMethodName)) {
				return;
			}

			JointPoint targetJP = new JointPoint();
			targetJP.setEditPoint(EditPoint.CALL);
			targetJP.setClassName(targetClassName);
			targetJP.setMethodName(targetMethodName);

			PointCut pointCut = config.getPointCut(targetJP);

			if (pointCut == null) {

				// descriptor を指定して再取得
				try {
					CtBehavior targetMethod = isConstructor
							? ((ConstructorCall) call).getConstructor()
							: call.getMethod();
					targetJP.setDescriptor(targetMethod.getMethodInfo().getDescriptor());
				} catch (NotFoundException e) {
					// クラスパスに呼び出し先が含まれていない場合は無視
					log.warn("呼び出し先 %s クラスパスなし。呼び出し元: %s", targetClassName, ctClass.getName());
					return;
				}
				pointCut = config.getPointCut(targetJP);
			}
			if (pointCut == null) {
				return;
			}
			if (!isEnabledLevel(pointCut.getLevel())) {
				return;
			}

			//---------------------------------------------------------------------
			// 呼び出し元の情報を取得
			//---------------------------------------------------------------------
			CtBehavior whereMethod = call.where();
			String whereClassName = whereMethod.getDeclaringClass().getName();
			String whereMethodName = whereMethod.getName();
			whereMethodName = optimizeConstructorName(whereClassName, whereMethodName);

			if (traceConfig == null || !traceConfig.disabled) {

				// 呼び出し元の場所による除外
				if (containsWhere(pointCut.getExcludeWheres(), whereClassName, whereMethodName)) {
					//log.debug("excludeWhere AOP除外： " + whereClassName + "#" + whereMethodName +
					//	" -> " + targetClassName + "#" + targetMethodName);
					return;
				}

				// 呼び出し元の場所による限定
				List<JointPoint> includeWheres = pointCut.getIncludeWheres();
				if (includeWheres.size() > 0 && !containsWhere(includeWheres, whereClassName, whereMethodName)) {
					//log.debug("includeWhere AOP限定： " + whereClassName + "#" + whereMethodName +
					//	" -> " + targetClassName + "#" + targetMethodName);
					return;
				}
			}

			String advice = pointCut.getAdvice();
			if (advice.contains("?{")) {
				JointPoint whereJP = new JointPoint();
				whereJP.setEditPoint(EditPoint.CALL);
				whereJP.setClassName(whereMethod.getDeclaringClass().getName());
				whereJP.setMethodName(whereMethod.getName());
				whereJP.setDescriptor(whereMethod.getMethodInfo().getDescriptor());
				advice = bindArguments(advice, whereJP, targetJP);
			}

			try {
				call.replace(advice);
				isEdited = true;

			} catch (CannotCompileException e) {
				log.warn("%s 呼び出し元: %s#%s、呼び出し先: %s#%s",
					e, whereClassName, whereMethodName, targetClassName, targetMethodName);
			}

		} finally {

			Analyses.end(TranslationEditor.class, "edit", start);
		}
	}

	/**
	 * 指定されたレベルが現在の実行環境で有効か判定します。
	 * <p>
	 * @return 有効な場合は true
	 */
	private boolean isEnabledLevel(String level) {

		if (level == null) {
			return true;
		}
		// 現状はログのレベルを使用しておく
		return log.isEnabled(Level.valueOf(level.toUpperCase()));
	}

	/**
	 * ジョイント・ポイントのリストにクラス名とメソッド名が含まれるか判定します。
	 *
	 * @param jointPointList ジョイント・ポイントのリスト
	 * @param whereClassName 呼び出し元クラス名
	 * @param whereMethodName 呼び出し元メソッド名
	 * @return 含まれる場合は true
	 */
	private boolean containsWhere(
			List<JointPoint> jointPointList, String whereClassName, String whereMethodName) {

		long start = System.nanoTime();

		try {
			// 現状は include、exclude の descriptor は未サポート
			for (JointPoint jointPoint : jointPointList) {

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

					// メソッド名は完全一致
					String mName = jointPoint.getMethodName();
					if (mName == null || whereMethodName.equals(mName)) {
						return true;
					}
				}
			}
			return false;

		} finally {

			Analyses.end(TranslationEditor.class, "containsWhere", start);
		}
	}

	/**
	 * アドバイスに含まれる ?{JOINT_POINT} などに実際の値を埋め込みます。
	 * <p>
	 * @param advice アドバイス
	 * @param whereJP 呼び出し元ジョイント・ポイント
	 * @param targetJP 呼び出し先ジョイント・ポイント
	 * @return 置換後のアドバイス文字列
	 */
	private String bindArguments(String advice, JointPoint whereJP, JointPoint targetJP) {

		long start = System.nanoTime();

		// ?{JOINT_POINT} を new JointPont(...) に置換
		if (advice.contains("?{JOINT_POINT}")) {
			advice = advice.replaceAll("\\?\\{JOINT_POINT\\}",
				Matcher.quoteReplacement(
					"new " + RuntimeJointPoint.class.getName() + "(" +
					EditPoint.class.getName() + "." + whereJP.getEditPoint().name() +
					",\"" + whereJP.getClassName() + "\"" +
					",\"" + whereJP.getMethodName() + "\"" +
					",\"" + whereJP.getDescriptor() + "\"" +
					", $0" +
					")"
				)
			);
		}
		// ?{TARGER_STRING} を呼び出し先を識別する文字列に置換
		if (advice.contains("?{TARGET_STRING}")) {
			advice = advice.replaceAll("\\?\\{TARGET_STRING\\}",
				Matcher.quoteReplacement(
					"\"" + targetJP.getClassName() +
					"#" + targetJP.getMethodName() +
					StringUtils.trimToEmpty(targetJP.getDescriptor()) + "\""
				)
			);
		}

		Analyses.end(TranslationEditor.class, "bindArguments", start);
		return advice;
	}

	/**
	 * コンストラクタまたはメソッドを編集します。
	 * editPoint が execution の場合に呼び出されます。
	 * <p>
	 * @param ctBehavior コンストラクタまたはメソッド・オブジェクト
	 * @throws CannotCompileException コンパイルできない場合
	 */
	public void editBehavior(CtBehavior ctBehavior) throws CannotCompileException {

		long start = System.nanoTime();

		try {
			String className = ctClass.getName();
			String methodName = ctBehavior.getName();
			methodName = optimizeConstructorName(className, methodName);

			JointPoint jointPoint = new JointPoint();
			jointPoint.setEditPoint(EditPoint.EXECUTION);
			jointPoint.setClassName(className);
			jointPoint.setMethodName(methodName);
			jointPoint.setDescriptor(ctBehavior.getMethodInfo().getDescriptor());

			PleiadesConfig mapping = PleiadesConfig.getInstance();
			PointCut pointCut = mapping.getPointCut(jointPoint);
			if (pointCut == null) {
				return;
			}
			if (!isEnabledLevel(pointCut.getLevel())) {
				return;
			}

			Timing timing = pointCut.getTiming();
			String advice = bindArguments(pointCut.getAdvice(), jointPoint, jointPoint);

			if (timing == Timing.BEFORE) {
				ctBehavior.insertBefore(advice);
			} else if (timing == Timing.AFTER) {
				ctBehavior.insertAfter(advice);
			} else {
				throw new IllegalStateException(
				"編集ポイントが execution の場合、timing は before または after " +
				"である必要があります。" + jointPoint);
			}
			isEdited = true;

		} finally {

			Analyses.end(TranslationEditor.class, "editBehavior", start);
		}
	}

	/**
	 * メソッド名がコンストラクターの場合に $ より前の部分を除去して最適化します。
	 * @param className クラス名
	 * @param methodName メソッド名
	 * @return 最適化されたコンストラクター名
	 */
	private String optimizeConstructorName(String className, String methodName) {

		if (
				className.contains("$") &&
				methodName.contains("$") &&
				!methodName.endsWith("$") &&
				className.endsWith(methodName)
		) {
			return methodName.substring(methodName.indexOf("$") + 1);
		}
		return methodName;
	}

	/**
	 * 編集結果をバイトコードで取得します。
	 * <p>
	 * @return バイトコード。未編集の場合は null。
	 * @throws IOException 入出力例外が発生した場合
	 * @throws CannotCompileException コンパイルできない場合
	 */
	public byte[] toBytecode() throws IOException, CannotCompileException {

		long start = System.nanoTime();
		try {
			return isEdited ? ctClass.toBytecode() : null;
		} finally {
			Analyses.end(TranslationEditor.class, "toBytecode", start);
		}
	}
}
