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

import java.io.File;
import java.io.IOException;
import java.lang.instrument.Instrumentation;
import java.util.Arrays;
import java.util.LinkedList;
import java.util.List;

import javassist.CannotCompileException;
import javassist.CtClass;
import javassist.CtMethod;
import javassist.NotFoundException;
import jp.sourceforge.mergedoc.pleiades.Pleiades;
import jp.sourceforge.mergedoc.pleiades.PleiadesOption;
import jp.sourceforge.mergedoc.pleiades.aspect.advice.PleiadesConfig;
import jp.sourceforge.mergedoc.pleiades.aspect.resource.CacheFiles;
import jp.sourceforge.mergedoc.pleiades.aspect.resource.DynamicTranslationDictionary;
import jp.sourceforge.mergedoc.pleiades.aspect.resource.ExcludeClassNameCache;
import jp.sourceforge.mergedoc.pleiades.aspect.resource.ExcludePackageProperties;
import jp.sourceforge.mergedoc.pleiades.aspect.resource.RegexDictionary;
import jp.sourceforge.mergedoc.pleiades.aspect.resource.TransformedClassCache;
import jp.sourceforge.mergedoc.pleiades.log.Logger;

import org.apache.commons.lang.ArrayUtils;

/**
 * Java アプリケーション起動時のバイトコード変換を行うトランスフォーマーです。
 * <p>
 * @author cypher256
 */
public class LauncherTransformer extends AbstractTransformer {

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

	/** このクラス名 (AOP static 呼び出し用) */
	private static final String THIS_CLASS = LauncherTransformer.class.getName();

	/** 変換済みクラス・キャッシュ */
	protected static volatile TransformedClassCache transformedClassCache;

	/** このクラスのインスタンス */
	private static LauncherTransformer thisInstance;

	/** 起動オプション：クリーン文字列 */
	private static final String CLEAN_OPTION_STRING = "-clean";

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

	/**
	 * 起動トランスフォーマーを構築します。
	 */
	public LauncherTransformer() {
		thisInstance = this;
		init();
	}

	/**
	 * 初期処理を行います。
	 */
	protected void init() {

		long start = System.nanoTime();

		// アスペクト定義をロード
		// 注) 別スレッドで処理すると SAXParser でデッドロックする
		PleiadesConfig.getInstance();

		// -clean に依存しないものをロード (非同期実行)
		// -clean に依存するもは startTranslationTransformer でロード
		AsyncQueue.add(new AsyncCommand("翻訳除外パッケージ・プロパティーのロード") {
			public void execute() {
				ExcludePackageProperties.getInstance();
			}
		});
		AsyncQueue.add(new AsyncCommand("正規表現翻訳プロパティーのロード") {
			public void execute() {
				RegexDictionary.getInstance();
			}
		});

		Analyses.end(LauncherTransformer.class, "init", start);
	}

	/**
	 * バイトコード変換を行います。
	 */
	@Override
	protected byte[] transform(
			String classId, String className, byte[] bytecode)
			throws CannotCompileException, NotFoundException, IOException {

		if (className.startsWith("jp.sourceforge.mergedoc.")) {
			return null;
		}

		try {
			CtClass clazz = createCtClass(bytecode);
			CtMethod main = clazz.getMethod("main", "([Ljava/lang/String;)V");

			main.insertBefore(
				"$1 = " + THIS_CLASS + ".start($$);" +
				THIS_CLASS + ".shutdownLauncherProcess();" +
				THIS_CLASS + ".addShutdownHook();"
			);

			 // Java アプリによってはメインスレッドは開始後にすぐに終了する場合があるため、
			 // シャットダウンフックで対応
			//main.insertAfter(THIS_CLASS + ".shutdown();");

			return clazz.toBytecode();

		} finally {

			removeTransformer();
		}
	}

	@Override
	protected void handleException(Throwable e, String msg) {
		log.error(e, msg);
	}

	/**
	 * このトランスフォーマーをエージェントから削除します。
	 */
	protected static void removeTransformer() {
		log.info("Pleiades AOP 起動トランスフォーマーを停止します。");
		Pleiades.getInstrumentation().removeTransformer(thisInstance);
	}

	/**
	 * 翻訳トランスフォーマーを開始します。
	 * @param args 起動オプション配列
	 * @return 起動オプション配列
	 */
	protected static String[] startTranslationTransformer(String... args) {

		long start = System.nanoTime();

		try {
			final PleiadesOption option = Pleiades.getPleiadesOption();

			// 起動引数の -clean を取得
			List<String> argList = new LinkedList<String>(Arrays.asList(args));
			boolean isIncludeArgsClean = argList.contains(CLEAN_OPTION_STRING);

			if (!option.isClean) {
				option.isClean = isIncludeArgsClean;
			}

			// キャッシュが無い場合は強制的に -claen を指定
			if (!option.isClean) {

				File excludeList = Pleiades.getResourceFile(CacheFiles.EXCLUDE_CLASS_LIST);
				if (!excludeList.exists()) {
					log.info("変換除外クラス名キャッシュが存在しないため、強制的に -clean モードで起動します。");
					option.isClean = true;
				}
			}
			log.info("アプリケーションの起動を開始しました。-clean:" + option.isClean);

			if (option.isClean) {
				CacheFiles.clear();
				if (!isIncludeArgsClean) {
					argList.add(CLEAN_OPTION_STRING);
				}
			} else {
				// -clean でない場合はこのトランスフォーマーをエージェントから削除
				// Main クラス以外は変換済みキャッシュを使用する
				removeTransformer();
			}

			// プロセス排他ロック (同期処理の中に入れると ClassCircularityError 発生するため先に実行)
			Locks.lock();

			AsyncQueue.add(new AsyncCommand("リソースのロード") {
				public void execute() {

					try {
						// 変換除外クラス名キャッシュをロード
						ExcludeClassNameCache.getInstance();

						// 変換済みクラス・キャッシュをロード
						transformedClassCache = TransformedClassCache.getInstance();

						// 翻訳辞書をロード
						DynamicTranslationDictionary.getInstance();

					} finally {

						// プロセス排他ロック解除
						Locks.release();
					}
				}
			});

			log.info("Pleiades AOP 翻訳トランスフォーマーを開始します。");
			Instrumentation inst = Pleiades.getInstrumentation();
			inst.addTransformer(new TranslationTransformer());

			return argList.toArray(new String[argList.size()]);

		} catch (Throwable e) {

			String msg = "Pleiades AOP 翻訳トランスフォーマーの開始に失敗しました。";
			log.fatal(msg);
			throw new IllegalStateException(msg, e);

		} finally {

			Analyses.end(LauncherEclipseTransformer.class, "startTranslationTransformer", start);
		}
	}

	// -------------------------------------------------------------------------------------------
	// 以下、Eclipse に埋め込んだ AOP により呼び出される public static メソッド

	/**
	 * 翻訳トランスフォーマーを開始します。
	 * @param args Eclipse 起動オプション配列
	 * @return 起動オプション配列
	 */
	public static String[] start(String... args) {

		startTranslationTransformer(args);

		// 引数に -clean がある場合は削除 (IDEA などは起動できなくなるため)
		String[] newArgs = (String[]) ArrayUtils.removeElement(args, CLEAN_OPTION_STRING);
		return newArgs;
	}

	/**
	 * 起動処理をシャットダウンします。
	 */
	public static void shutdownLauncherProcess() {

		AsyncQueue.add(new AsyncCommand("非同期キューのシャットダウン") {
			public void execute() throws Exception {
				AsyncQueue.shutdown();
			}
		});
	}

	/**
	 * シャットダウンフックを追加します。<br>
	 * Java アプリによってはメインスレッドは開始後にすぐに終了する場合があるため、
	 * シャットダウンフックで対応します。
	 */
	public static void addShutdownHook() {

		Runtime.getRuntime().addShutdownHook(new Thread() {
			public void run() {
				shutdown();
			}
		});

		// javaw の場合、シャットダウンフックが動作しないため、AWT フレームをパック
		new java.awt.Frame().pack();
	}

	/**
	 * 各種リソースを保存し、Pleiades をシャットダウンします。
	 */
	public static void shutdown() {

		new Thread("pleiades-shutdown") {
			@Override
			public void run() {

				if (log.isDebugEnabled()) {
					Analyses.flashLog("Eclipse 終了。");
				}

				try {
					// プロセス排他ロック
					Locks.lock();

					// 変換済みクラス・キャッシュを保存
					TransformedClassCache.getInstance().shutdown();

					// 変換除外クラス名キャッシュを保存
					ExcludeClassNameCache.getInstance().shutdown();

					// 翻訳プロパティーをキャッシュとして保存
					DynamicTranslationDictionary.getInstance().shutdown();

				} catch (Exception e) {
					log.warn(e, "キャッシュの更新に失敗しました。");

				} finally {

					// プロセス排他ロック解除
					Locks.release();

					// 非同期ユーティリティーのシャットダウン (通常はシャットダウン済み)
					AsyncQueue.shutdown();

					// プリロード用の自動終了
					if (Pleiades.getPleiadesOption().isPreload()) {
						System.exit(0);
					}
				}
			}
		}.start();
	}

	/**
	 * デバッグログを出力します。
	 * @param o オブジェクト
	 */
	public static void debug(Object o) {
		log.debug(String.valueOf(o));
	}
}
