/*
 * 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.File;
import java.io.IOException;
import java.lang.instrument.Instrumentation;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

import javassist.CannotCompileException;
import javassist.CtClass;
import javassist.CtMethod;
import javassist.NotFoundException;
import jp.sourceforge.mergedoc.org.apache.commons.lang.ArrayUtils;
import jp.sourceforge.mergedoc.org.apache.commons.lang.StringUtils;
import jp.sourceforge.mergedoc.pleiades.Pleiades;
import jp.sourceforge.mergedoc.pleiades.PleiadesOption;
import jp.sourceforge.mergedoc.pleiades.log.FileLogger;
import jp.sourceforge.mergedoc.pleiades.log.Logger;
import jp.sourceforge.mergedoc.pleiades.resource.PropertySet;
import jp.sourceforge.mergedoc.pleiades.runtime.TranslationTransformer.LoggingDescTranslationTransformer;
import jp.sourceforge.mergedoc.pleiades.runtime.advice.PleiadesConfig;
import jp.sourceforge.mergedoc.pleiades.runtime.advice.TraceConfig;
import jp.sourceforge.mergedoc.pleiades.runtime.resource.CacheFiles;
import jp.sourceforge.mergedoc.pleiades.runtime.resource.CallHierarchyExplorer;
import jp.sourceforge.mergedoc.pleiades.runtime.resource.ExcludeClassNameCache;
import jp.sourceforge.mergedoc.pleiades.runtime.resource.ExcludePackageProperties;
import jp.sourceforge.mergedoc.pleiades.runtime.resource.RegexDictionary;
import jp.sourceforge.mergedoc.pleiades.runtime.resource.RuntimeTranslationDictionary;
import jp.sourceforge.mergedoc.pleiades.runtime.resource.TransformedClassCache;

/**
 * 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();
		final File pleiadesConfigurationPath = Applications.getPleiadesConfigurationPath();

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

		// 非同期実行 (待ち無し)
		AsyncQueue.add(new AsyncCommand("-clean に依存しないプロパティーのロード") {
			public void execute() throws Exception {

				// -clean に依存しないものをロード
				// -clean に依存するもは startTranslationTransformer でロード
				ExcludePackageProperties.getInstance();
				RegexDictionary.getInstance();

				// 初回起動時実行コマンドの実行 (Windows のみ使用可能)
				String initBatch = Pleiades.getPleiadesOption().initBatch;
				if (initBatch != null) {
					File initBatchMarkFile = new File(pleiadesConfigurationPath, ".init.batch.processed");
					if (!initBatchMarkFile.exists()) {
						initBatchMarkFile.createNewFile();

						String cmd = "cmd /c start " + initBatch.replace('/', '\\');
						File currentDir = new File(Applications.getHome(), initBatch.replaceFirst("[^/\\\\]+$", ""));
						log.info("初回起動時実行コマンド実行 コマンド:" + cmd + " カレントディレクトリ:" + currentDir);
						Runtime.getRuntime().exec(cmd, null, currentDir);
					}
				}
			}
		});

		// 非同期実行 (待ち有り) -clean 設定
		AsyncQueue.addAwait(new AsyncCommand("システムプロパティをファイル出力") {
			public void execute() throws Exception {

				File sysPropFile = new File(pleiadesConfigurationPath, "system.properties");
				PleiadesOption pleiadesOption = Pleiades.getPleiadesOption();

				// 起動オプション debug ON
				if (log.isDebugEnabled()) {
					// debug ON 時の出力ファイルが無い場合は debug 指定が変更されたとみなし、-clean ON にする
					if (!pleiadesOption.isClean && !sysPropFile.exists()) {
						pleiadesOption.isClean = true;
						log.info("-clean を有効にしました。(起動オプション debug OFF -> ON 変更検出)");
					}
					// sysPropFile 出力
					new PropertySet(System.getProperties()).store(sysPropFile, "System Properties");
				}
				// 起動オプション debug OFF
				else {
					// debug ON 時の出力ファイルが有る場合は debug 指定が変更されたとみなし、-clean ON にする
					if (!pleiadesOption.isClean && sysPropFile.exists()) {
						sysPropFile.delete();
						pleiadesOption.isClean = true;
						log.info("-clean を有効にしました。(起動オプション debug ON -> OFF 変更検出)");
					}
				}
			}
		});

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

	/**
	 * 起動時のバイトコード変換を行います。
	 */
	@Override
	protected byte[] transformBytecode(
			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 + ".addShutdownHook();" +
				THIS_CLASS + ".shutdownLauncherProcess();"
			);

			 // 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 ArrayList<String>(Arrays.asList(args));
			boolean isIncludeArgsClean = argList.contains(CLEAN_OPTION_STRING);

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

			// キャッシュが無い場合は強制的に -clean を指定
			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();

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

					} finally {

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

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

			TraceConfig traceConfig = PleiadesConfig.getInstance().traceConfig;
			if (traceConfig != null && StringUtils.isNotEmpty(traceConfig.logDescClass)) {
				inst.addTransformer(new LoggingDescTranslationTransformer(traceConfig.logDescClass));
			} else {
				inst.addTransformer(new TranslationTransformer());
			}

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

		} catch (Throwable e) {

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

		} finally {

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

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

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

		// 非同期実行キュー終了待ち (プラグイン更新判定待ち)
		AsyncQueue.awaitTermination();

		// 上記を待って -clean 確定後に処理
		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() {

		// Eclipse 以外の処理
		// IDEA などで起動時にシャットダウンフックが呼ばれ、終了してしまう場合があるため、登録前に待機
		AsyncQueue.add(new AsyncCommand("リソースのロード") {
			public void execute() {

				try {
					Thread.sleep(1000 * 10);
				} catch (InterruptedException e) {
				}
				log.debug("Java シャットダウンフックを登録します。");
				Runtime.getRuntime().addShutdownHook(new Thread() {
					public void run() {
						log.debug("Java シャットダウンフックが呼び出されました。");
						shutdown();
					}
				});

				if (log.isDebugEnabled()) {
					try {
						Thread.sleep(1000 * 10);
					} catch (InterruptedException e) {
					}
					Analyses.flashLog("スタートアップ時の計測");
				}
			}
		});

		// IDEA 起動時のエラー回避 - UIDefaults.getUI() failed: no ComponentUI class for
		// Eclipse 4.6 で確認した限り、-vm で javaw を指定していても shutdown が呼び出されるようになっている
		/*
		// javaw の場合、シャットダウンフックが動作しないため、AWT フレームをパック
		new java.awt.Frame().pack();
		*/
	}

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

		if (log.isDebugEnabled()) {
			Analyses.flashLog("Pleiades をシャットダウンします。");
		}

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

			// AOP でキャッシュ破棄が指示されていた場合
			if (RuntimeTranslationDictionary.getInstance().isInvalidateCache) {

				// キャッシュをクリア
				CacheFiles.clear();
			}
			else {
				// 変換済みクラス・キャッシュを保存
				TransformedClassCache.getInstance().shutdown();

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

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

			// デバッグ時の getStackTrace() 呼び出しトップ n をログに出力
			CallHierarchyExplorer.getInstance().flushStackTraceCount();

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

		} finally {

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

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

			// ログファイルのクローズ
			FileLogger.close();

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

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