/*
 * 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.reflect.Method;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;
import java.util.concurrent.atomic.AtomicInteger;

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.pleiades.Pleiades;
import jp.sourceforge.mergedoc.pleiades.PleiadesOption;
import jp.sourceforge.mergedoc.pleiades.log.Logger;
import jp.sourceforge.mergedoc.pleiades.resource.Files;
import jp.sourceforge.mergedoc.pleiades.resource.PropertySet;

/**
 * Eclipse 起動時のバイトコード変換を行うトランスフォーマーです。
 * <p>
 * @author cypher256
 */
public class LauncherEclipseTransformer extends LauncherTransformer {

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

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

	/** 始動時のワークスペース・ダイアログ表示有無 */
	private static volatile boolean showWorkspaceSelectionDialog = true;

	/** バイトコード変換済みクラス数 */
	private AtomicInteger transformedClassCount = new AtomicInteger();

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

		long start = System.nanoTime();

		AsyncQueue.add(new AsyncCommand("コア・プラグインを AOP クラスパスへ追加") {
			public void execute() throws Exception {

				// バージョンが新しい jar を優先するため名前逆順ソート
				File plugins = new File(Applications.getOsgiHome(), "plugins");
				File[] jars = plugins.listFiles(Files.createSuffixFilter(".jar"));
				Arrays.sort(jars);
				ArrayUtils.reverse(jars);
				Set<String> pluginSuffixSet = new HashSet<String>();

				for (File jar : jars) {

					String jarFileName = jar.getName();
					String pluginSuffix = jarFileName.replaceFirst("^(.+_).+$", "$1");
					if (pluginSuffixSet.contains(pluginSuffix)) {
						continue;
					}
					pluginSuffixSet.add(pluginSuffix);
					addClassPath(jar);
				}
			}
		});

		final PleiadesOption option = Pleiades.getPleiadesOption();
		if (!option.isClean) {
			AsyncQueue.addAwait(new AsyncCommand("プラグイン最終更新時刻の判定") {
				public void execute() throws Exception {

					File plugins = new File(Applications.getOsgiHome(), "plugins");
					File dropins = new File(Applications.getOsgiHome(), "dropins");

					if (Applications.isUpdated(plugins, dropins)) {
						option.isClean = true;
						log.info("Eclipse プラグインの更新が検出されたため、強制的に -clean モードで起動します。");
					}
				}
			});
		}

		super.init();

		AsyncQueue.add(new AsyncCommand("Eclipse バージョンの取得") {
			public void execute() {
				Applications.getVersion();
			}
		});

		AsyncQueue.add(new AsyncCommand("始動時のワークスペース・ダイアログ表示有無取得") {
			public void execute() {
				File prefs = Pleiades.getResourceFile("../.settings/org.eclipse.ui.ide.prefs");
				if (prefs.exists()) {
					PropertySet p = new PropertySet(prefs);
					showWorkspaceSelectionDialog = Boolean.valueOf(p.get("SHOW_WORKSPACE_SELECTION_DIALOG"));
				}
			}
		});

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

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

		if (!className.startsWith("org.eclipse.")) {
			return null;
		}
		byte[] transformedBytecode = null;

		// -----------------------------------------------------------------------------------
		// Main クラスの変換
		// -----------------------------------------------------------------------------------
		// 3.2 以前： org.eclipse.core.launcher.Main
		// 3.3 以降： org.eclipse.equinox.launcher.Main
		if (className.endsWith(".launcher.Main")) {

			log.info("transform: " + className);

			try {
				// 翻訳トランスフォーマーの開始処理を追加
				CtClass clazz = createCtClass(bytecode);
				CtMethod basicRun = clazz.getMethod("basicRun", "([Ljava/lang/String;)V");
				basicRun.insertBefore("$1 = " + THIS_CLASS + ".start($$);");
				// basicRun.insertAfter(CLASS_NAME + ".shutdown();", true);
				// ↑終了しないうちに起動してしまうのを防ぐため、Workbench クラスに移動

				// クリーン・メッセージの表示
				CtMethod getSplashLocation = clazz.getMethod("getSplashLocation", "([Ljava/net/URL;)Ljava/lang/String;");
				getSplashLocation.insertAfter(THIS_CLASS + ".showCleanMessage($_);");

				// キャッシュしない
				// キャッシュのロードに -clean の判定が必要だが、この時点では -clean 判定できない
				return clazz.toBytecode();

			} catch (NotFoundException e) {

				// Eclipse 3.3 以降、Eclipse アプリケーションとして起動した場合、
				// org.eclipse.core.launcher.Main は main メソッドしかなく、このクラスから
				// org.eclipse.equinox.launcher.Main#main が呼び出される。
				// 最初の Main でのメソッドなしは無視
				if (className.equals("org.eclipse.core.launcher.Main")) {
					return null;
				}
				throw e;
			}
		}
		// -----------------------------------------------------------------------------------
		// WorkbenchAdvisor クラスの変換
		// -----------------------------------------------------------------------------------
		// Eclipse 4.7 以降で IDEWorkbenchAdvisor のコンストラクターが増えたため、親クラスに変更
		//else if (className.equals("org.eclipse.ui.internal.ide.application.IDEWorkbenchAdvisor")) {
		else if (className.equals("org.eclipse.ui.application.WorkbenchAdvisor")) {

			log.info("transform: " + className);
			transformedClassCount.incrementAndGet();

			// 新規ワークスペース作成時の metadeta コピー処理を追加
			CtClass clazz = createCtClass(bytecode);
			clazz.getDeclaredConstructor(null).insertBefore(

				"org.eclipse.osgi.service.datalocation.Location loc = null;" +
				"loc = org.eclipse.core.runtime.Platform.getInstanceLocation();" +
				"java.net.URL workspace = null;" +
				"if (loc != null) workspace = loc.getURL();" +
				Metadata.class.getName() + ".getInstance().createNewWorkspaceMetadata(workspace);"
			);
			transformedBytecode = clazz.toBytecode();
		}
		// -----------------------------------------------------------------------------------
		// Workbench クラスの変換
		// -----------------------------------------------------------------------------------
		else if (className.equals("org.eclipse.ui.internal.Workbench")) {

			log.info("transform: " + className);
			transformedClassCount.incrementAndGet();

			// 終了時のシャットダウン処理を追加
			CtClass clazz = createCtClass(bytecode);
			CtMethod shutdown = clazz.getMethod("shutdown", "()V");
			shutdown.insertBefore(THIS_CLASS + ".shutdown();");

			// ワークベンチ作成完了後の処理
			// ⇒ E4 では runUI の最後で呼ばれる runEventLoop が無くなった
			// ⇒ E3.7 で -Xverify:none を指定すると Display が CannotCompileException
			/*
			CtMethod runEventLoop = clazz.getMethod("runEventLoop",
				"(Lorg/eclipse/jface/window/Window$IExceptionHandler;Lorg/eclipse/swt/widgets/Display;)V");
			runEventLoop.insertBefore(CLASS_NAME + ".onWorkbenchCreationComplete($0);");
			*/
			CtMethod addStartup = clazz.getMethod("addStartupRegistryListener", "()V");
			addStartup.insertAfter(THIS_CLASS + ".onWorkbenchCreationComplete($0);");

			transformedBytecode = clazz.toBytecode();
		}
		// -----------------------------------------------------------------------------------
		// AbsolutePositionProgressMonitorPart クラスの変換
		// -----------------------------------------------------------------------------------
		else if (className.equals("org.eclipse.ui.splash.BasicSplashHandler$AbsolutePositionProgressMonitorPart")) {

			log.info("transform: " + className);
			transformedClassCount.incrementAndGet();

			// Eclipse 4 のスプラッシュ進捗バーの進捗が少なすぎる問題を修正
			String eclipseVersion = Applications.getVersion();
			if (eclipseVersion.matches("4\\.[0-3]")) {

				CtClass clazz = createCtClass(bytecode);
				CtMethod getBundleCount = clazz.getMethod("internalWorked", "(D)V");
				getBundleCount.insertBefore("$1 *= 4;");

				transformedBytecode = clazz.toBytecode();
			}
		}
		// -----------------------------------------------------------------------------------
		// ResourcesPlugin クラスの変換
		// -----------------------------------------------------------------------------------
		else if (className.equals("org.eclipse.core.resources.ResourcesPlugin")) {

			log.info("transform: " + className);
			transformedClassCount.incrementAndGet();

			// エンコーディング未設定時のデフォルト設定
			CtClass clazz = createCtClass(bytecode);
			CtMethod method = clazz.getMethod("getPlugin", "()Lorg/eclipse/core/resources/ResourcesPlugin;");

			method.insertAfter(
				"String enc = " + Metadata.class.getName() + ".getInstance().getDefaultWorkspaceEncoding();" +
				"if (enc != null) " +
				"	$_.getPluginPreferences().setValue(\"encoding\", enc);"
			);
			transformedBytecode = clazz.toBytecode();
		}
		// -----------------------------------------------------------------------------------
		// WorkbenchWindows クラスの変換
		// -----------------------------------------------------------------------------------
		else if (className.equals("org.eclipse.ui.internal.WorkbenchWindow")) {

			log.info("transform: " + className);
			transformedClassCount.incrementAndGet();

			try {
				// preload の場合はウィンドウを座標外に表示
				CtClass clazz = createCtClass(bytecode);
				CtMethod restore = clazz.getMethod("restoreState",
					"(Lorg/eclipse/ui/IMemento;Lorg/eclipse/ui/IPerspectiveDescriptor;)Lorg/eclipse/core/runtime/IStatus;");
				restore.insertBefore(THIS_CLASS + ".setWindowPosition($1);");
				transformedBytecode = clazz.toBytecode();

			} catch (NotFoundException e) {

				// E4 ではこのクラスが大幅に変更されているため、未対応とする
			}
		}
		// -----------------------------------------------------------------------------------

		// 次回起動用にキャッシュ
		if (transformedBytecode != null && transformedClassCache != null) {
			transformedClassCache.putNextLaunch(classId, transformedBytecode);
		}
		if (transformedClassCount.get() == 5) {
			removeTransformer();
		}
		return transformedBytecode;
	}

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

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

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

		// 上記を待って -clean 確定後に処理
		String[] newArgs = LauncherTransformer.startTranslationTransformer(args);

		if (!Pleiades.getPleiadesOption().isDefaultSplash) {
			AsyncQueue.add(new AsyncCommand("スプラッシュ画像パスの設定") {
				public void execute() {
					String splashLocation = getSplashLocation();
					log.debug("スプラッシュ・ロケーション: %s", splashLocation);
					if (splashLocation != null) {
						System.setProperty("osgi.splashLocation", splashLocation);
					}
				}
			});
		}

		return newArgs;
	}

	/**
	 * クリーン・メッセージを表示します。
	 * @param splashLocation スプラッシュ・パス
	 */
	public static void showCleanMessage(String splashLocation) {

		if (Pleiades.getPleiadesOption().isClean) {
			CleanMessage.show(splashLocation);
		}
	}

	/**
	 * Eclipse ワークベンチ作成完了後の処理です。
	 * @param workbench org.eclipse.ui.internal.Workbench インスタンス
	 */
	public static void onWorkbenchCreationComplete(final Object workbench) throws Exception {

		final PleiadesOption option = Pleiades.getPleiadesOption();

		if (option.isClean) {
			AsyncQueue.add(new AsyncCommand("クリーン・スプラッシュのクローズ") {
				public void execute() {
					CleanMessage.close();
				}
			});
		}

		if (log.isDebugEnabled()) {
			AsyncQueue.add(new AsyncCommand("実行時システム・プロパティーの保管") {
				public void execute() {
					PropertySet systemProp = new PropertySet(System.getProperties());
					File file = Pleiades.getResourceFile("system.properties");
					systemProp.store(file, "実行時システム・プロパティー");
				}
			});
		}

		if (option.isPreload()) {
			AsyncQueue.add(new AsyncCommand("プリロード自動終了呼び出し") {
				public void execute() throws Exception {
					Method close = workbench.getClass().getMethod("close");
					close.invoke(workbench);
				}
			});
		}

		// Eclipse 始動計測時間をログに出力
		long startTime = Long.valueOf(System.getProperty("eclipse.startTime"));
		long curTime = System.currentTimeMillis();
		final double startupTime = (curTime - startTime) / 1e+3;
		String message = String.format("Eclipse 始動完了 - 始動時間: %.3f 秒", startupTime);
		Analyses.flashLog(message + "。-clean:" + option.isClean);

		// Eclipse 始動計測時間をステータスバーに出力
		if (!showWorkspaceSelectionDialog) {

			try {
				Method getActivatedWindow = workbench.getClass().getDeclaredMethod("getActivatedWindow");
				getActivatedWindow.setAccessible(true);
				Object workbenchWindow = getActivatedWindow.invoke(workbench);
				Method setStatus = workbenchWindow.getClass().getMethod("setStatus", String.class);

				// Eclipse UI 操作は非同期で行うと SWTException: Invalid thread access が発生するため同期処理
				setStatus.invoke(workbenchWindow, message);

			} catch (NoSuchMethodException e) {
				// E4 ではメソッドが無くなり、他の方法で取得すると null のためサポートしない
				log.debug("E4 のためステータスバーに始動時間を表示しません。");
			}
		}

		// 非同期キューをシャットダウン
		shutdownLauncherProcess();
	}

	/**
	 * ワークベンチ復元前の処理です。
	 * @param memento org.eclipse.ui.IMemento インスタンス
	 */
	public static void setWindowPosition(Object memento) throws Exception {

		if (Pleiades.getPleiadesOption().isPreload()) {

			// プリロード起動時の表示位置に座標外の値をセット
			Method putInteger = memento.getClass().getMethod("putInteger", String.class, int.class);
			putInteger.invoke(memento, "width", 200);
			putInteger.invoke(memento, "x", -199);
		}
	}

	/**
	 * スプラッシュ画像パスを取得します。
	 * @return スプラッシュ画像パス。RCP の場合は null。
	 */
	public static String getSplashLocation() {
		// public にしないと javassist.CannotCompileException が発生する

		// RCP の場合、Pleiades スプラッシュは使用しない
		File home = Applications.getHome();
		if (
			// Windows
			!new File(home, "eclipse.exe").exists() &&
			// Mac Bundle (Eclipse 4.5 以降)
			!home.getName().equals("Eclipse") &&
			// Linux
			!new File(home, "eclipse").exists()
		) {
			return null;
		}

		// ユーザー・カスタム・スプラッシュ (以前に AIO エディション別スプラッシュで使用)
		// (pleiades を格納した plugins ディレクトリーがある場所)
		File customFile = Files.conf("../../../splash.bmp");
		if (customFile.exists()) {
			return customFile.getAbsolutePath().replace('\\', '/');
		}

		// Eclipse バージョンにあった bmp ファイル・パスを取得
		File file = Files.conf("images/splash" + Applications.getVersion() + ".bmp");
		if (!file.exists()) {
			file = Files.conf("images/splash.bmp");
		}
		String splashLocation = file.getAbsolutePath().replace('\\', '/');
		return splashLocation;
	}

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

		new Thread("pleiades-shutdown") {
			@Override
			public void run() {
				shutdown();
			}
		}.start();
	}
}
