/*
 * 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.ByteArrayInputStream;
import java.io.File;
import java.io.IOException;
import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.IllegalClassFormatException;
import java.net.URL;
import java.security.CodeSource;
import java.security.ProtectionDomain;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;

import javassist.CannotCompileException;
import javassist.ClassPool;
import javassist.CtBehavior;
import javassist.CtClass;
import javassist.NotFoundException;
import jp.sourceforge.mergedoc.pleiades.log.Logger;
import jp.sourceforge.mergedoc.pleiades.resource.Files;
import jp.sourceforge.mergedoc.pleiades.runtime.resource.ExcludeClassNameCache;
import jp.sourceforge.mergedoc.pleiades.runtime.resource.TransformedClassCache;

/**
 * バイトコード変換を行うための抽象クラスです。
 * このクラスは状態を保持しますが、同期化されていません。
 * <p>
 * @author cypher256
 */
abstract public class AbstractTransformer implements ClassFileTransformer {

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

	/** Javassist クラス・プール */
	private static final ClassPool classPool = new ClassPool(true);

	/** クラス・プールのクラスパスに追加済みロケーション・パス・セット */
	private static final Set<String> appendedLocationPathSet = Collections.synchronizedSet(new HashSet<String>());

	/** バイトコードを変換するトランスフォーマーブローカー */
	private DefaultTransformerBroker transformer = log.isDebugEnabled()
			? new AnalysesTransformerBroker()
			: new DefaultTransformerBroker();

	/** デフォルトのトランスフォーマーブローカークラス */
	private class DefaultTransformerBroker {

		public byte[] execute(String vmClassName,
				ProtectionDomain domain,
				byte[] bytecode) throws IllegalClassFormatException {

			String pluginLocation = getPluginLocation(vmClassName, domain);
			if (pluginLocation != null) {
				return transform(vmClassName, bytecode, pluginLocation);
			}
			return null;
		}

		protected String getPluginLocation(String vmClassName, ProtectionDomain domain)
				throws IllegalClassFormatException {

			try {
				String pluginLocation = "";

				// バイトコード変換しないパッケージ
				if (
					// null の場合がある
					vmClassName == null ||
					// java. 除外 (awt や javax.swing などは除外しない)
					(vmClassName.startsWith("java/") && !vmClassName.startsWith("java/awt/")) ||
					// JavaScript Nashorn
					vmClassName.startsWith("jdk/") ||
					vmClassName.startsWith("sun/") ||
					vmClassName.startsWith("com/sun/") ||
					vmClassName.startsWith("org/apache/commons")
				) {
					return null;
				}

				// トランスフォーマー内で使用するクラスはロード処理がループするため除外
				// その他の pleiades のクラスを含めているのは計測処理埋め込みのため
				if (vmClassName.startsWith("jp/sourceforge/mergedoc/pleiades/runtime/resource/")) {
					if (
						vmClassName.endsWith(TransformedClassCache.class.getSimpleName()) ||
						vmClassName.endsWith(ExcludeClassNameCache.class.getSimpleName())
					) {
						return null;
					}
				}

				if (domain != null) {

					CodeSource codeSource = domain.getCodeSource();
					if (codeSource != null) {

						URL location = codeSource.getLocation();
						if (location == null) {

							// IDEA の場合、location が null のため、クラスローダー文字列表現のプラグイン名を使用
							ClassLoader classLoader = domain.getClassLoader();
							if (classLoader.getClass().getSimpleName().equals("PluginClassLoader")) {
								pluginLocation = classLoader.toString();
							}
						}
						else {

							// Eclipse ヘルプ HTTP 500 エラー回避
							// jasper プラグインはバイトコード変換しない
							String locationPath = location.getPath();
							if (locationPath.contains("org.apache.jasper_")) {
								return null;
							}

							// クラス・ロケーションの取得 (jar ファイル名)
							int lastSlashPos = locationPath.lastIndexOf('/');
							if (lastSlashPos != -1) {
								pluginLocation = locationPath.substring(lastSlashPos + 1);

								// バイトコード変換に必要な関連クラスパスをクラス・プールに追加。
								// パフォーマンスのために先に追加済みか検査する。
								if (!appendedLocationPathSet.contains(pluginLocation)) {

									if (isClassPathTarget(locationPath, pluginLocation)) {
										addClassPath(locationPath, pluginLocation);
									}
								}
							}
						}
					}
				}
				return pluginLocation;

			} catch (Throwable e) {

				String msg = "バイトコード変換事前処理に失敗しました。" + vmClassName;
				log.error(e, msg);
				throw new IllegalClassFormatException(msg + " 原因:" + e);
			}
		}

		protected byte[] transform(String vmClassName, byte[] bytecode, String pluginLocation) {

			try {
				// バイトコード変換テンプレート・メソッドの呼び出し
				String className = vmClassName.replace('/', '.');
				String classId = pluginLocation + "/" + className;
				return transformBytecode(classId, className, bytecode);

			} catch (Throwable e) {

				StringBuilder msg = new StringBuilder();
				msg.append("バイトコード変換不可 ");
				msg.append(pluginLocation);
				msg.append("/");
				msg.append(vmClassName);
				msg.append(" ");
				msg.append(e);
				msg.append(" ");
				msg.append(e.getStackTrace()[0]);

				Throwable cause = e;
				while ((cause = cause.getCause()) != null) {
					msg.append(" => ");
					msg.append(cause);
				}

				handleException(e, msg.toString());
				return null;
			}
		}

		protected CtClass createCtClass(byte[] bytecode) throws IOException, NotFoundException {
			CtClass ctClass = classPool.makeClass(new ByteArrayInputStream(bytecode));
			ctClass.detach();
			return ctClass;
		}

		protected CtClass createCtClass(String className) throws IOException, NotFoundException {
			CtClass ctClass = classPool.makeClass(className);
			ctClass.detach();
			return ctClass;
		}
	}

	/** デバッグ分析用のトランスフォーマーブローカークラス */
	private class AnalysesTransformerBroker extends DefaultTransformerBroker {

		@Override
		protected String getPluginLocation(String vmClassName, ProtectionDomain domain) throws IllegalClassFormatException {

			long start = System.nanoTime();
			try {
				return super.getPluginLocation(vmClassName, domain);
			} finally {
				Analyses.end(DefaultTransformerBroker.class, "getPluginLocation", start);
			}
		}

		@Override
		protected byte[] transform(String vmClassName, byte[] bytecode, String pluginLocation) {
			long start = System.nanoTime();
			try {
				return super.transform(vmClassName, bytecode, pluginLocation);
			} finally {
				Analyses.end(DefaultTransformerBroker.class, "transform", start);
			}
		}

		@Override
		protected CtClass createCtClass(byte[] bytecode) throws IOException, NotFoundException {
			long start = System.nanoTime();
			try {
				return super.createCtClass(bytecode);
			} finally {
				Analyses.end(DefaultTransformerBroker.class, "createCtClass(bytecode)", start);
			}
		}

		@Override
		protected CtClass createCtClass(String className) throws IOException, NotFoundException {
			long start = System.nanoTime();
			try {
				return super.createCtClass(className);
			} finally {
				Analyses.end(DefaultTransformerBroker.class, "createCtClass(className)", start);
			}
		}
	}

	/**
	 * バイトコードを変換します。
	 */
	@Override
	final public byte[] transform(
			ClassLoader loader,
			String vmClassName,
			Class<?> classBeingRedefined,
			ProtectionDomain domain,
			byte[] bytecode) throws IllegalClassFormatException {

		return transformer.execute(vmClassName, domain, bytecode);
	}

	/**
	 * バイトコード変換の例外が発生した場合の処理を行います。
	 * @param e 例外
	 * @param msg メッセージ
	 */
	protected void handleException(Throwable e, String msg) {

		// プラグイン開発で ViewPart などを使用していると発生する場合あり
		//log.error(e, msg);
		//throw new IllegalClassFormatException(msg + " 原因:" + e);
		log.debug(msg);
	}

	/**
	 * クラスパス追加対象か判定します。
	 * <p>
	 * @param fullPath フルパス
	 * @param jarFileName 追加対象の場合は true
	 * @throws IOException 入出力例外が発生した場合
	 * @throws NotFoundException クラスが見つからない場合
	 */
	protected boolean isClassPathTarget(String fullPath, String jarFileName) throws IOException, NotFoundException {
		return false;
	}

	/**
	 * Javassist クラスパスを追加します。
	 * <p>
	 * @param fullPath フルパス (スラッシュ区切り)
	 * @param jarFileName JAR ファイル名
	 * @throws NotFoundException 指定されたパスが見つからない場合
	 */
	protected void addClassPath(String fullPath, String jarFileName) throws NotFoundException {

		appendedLocationPathSet.add(jarFileName);
		String classPath = Files.decodeURL(fullPath);
		classPool.appendClassPath(classPath);
	}

	/**
	 * Javassist クラスパスに jar ファイルを追加します。
	 * @param jarFile jar ファイル
	 */
	protected void addClassPath(File jarFile) throws IOException, NotFoundException {

		String jarFullPath = jarFile.getCanonicalPath().replace('\\', '/');
		addClassPath(jarFullPath, jarFile.getName());
	}

	/**
	 * バイトコードを変換するためのテンプレート・メソッドです。
	 * <p>
	 * @param classId クラス識別子
	 * @param className クラス名
	 * @param bytecode バイトコード
	 * @return 変換後のバイトコード。変換する必要が無い場合は null。
	 * @throws CannotCompileException クラスがコンパイル出来ない場合
	 * @throws NotFoundException クラスが見つからない場合
	 * @throws IOException 入出力例外が発生した場合
	 */
	abstract protected byte[] transformBytecode(
			String classId,
			String className,
			byte[] bytecode
	) throws CannotCompileException, NotFoundException, IOException;

	/**
	 * バイトコードを指定して CtClass オブジェクトを作成します。
	 * <p>
	 * @param bytecode バイトコード
	 * @return CtClass オブジェクト
	 * @throws IOException 入出力例外が発生した場合
	 * @throws NotFoundException クラスが見つからない場合
	 */
	protected CtClass createCtClass(byte[] bytecode) throws IOException, NotFoundException {
		return transformer.createCtClass(bytecode);
	}

	/**
	 * クラス名を指定して CtClass オブジェクトを作成します。
	 * <p>
	 * @return className クラス名
	 * @throws IOException 入出力例外が発生した場合
	 * @throws NotFoundException クラスが見つからない場合
	 */
	protected CtClass createCtClass(String className) throws IOException, NotFoundException {
		return transformer.createCtClass(className);
	}

	/**
	 * 指定された CtClass オブジェクトが持つすべてのコンストラクタおよび
	 * メソッド記述子をログに出力します。
	 * <p>
	 * @param ctClass CtClass オブジェクト
	 */
	protected void logDescriptor(CtClass ctClass) {

		for (CtBehavior behavior : ctClass.getDeclaredBehaviors()) {
			log.info("DESCRIPTOR: " +
					ctClass.getName() + "#" +
					behavior.getName() + " " +
					behavior.getMethodInfo().getDescriptor());
		}
	}
}
