package cn.forevermc.launcher.core;

import cn.forevermc.launcher.ForeverMC;
import cn.forevermc.launcher.bean.Config;
import cn.forevermc.launcher.bean.Game;
import cn.forevermc.launcher.bean.LaunchGame;
import cn.forevermc.launcher.bean.OpenJDK;
import cn.forevermc.launcher.bean.Page;
import cn.forevermc.launcher.bean.RuntimeDownloadSource;
import cn.forevermc.launcher.ctrl.pane.CanvasProgress;
import cn.forevermc.launcher.ctrl.pane.Dialog;
import cn.forevermc.launcher.ctrl.pane.GameOption;
import cn.forevermc.launcher.ctrl.pane.PlayerSelect;
import cn.forevermc.launcher.ctrl.pane.ResourceDownload;
import cn.forevermc.launcher.service.APISettingService;
import cn.forevermc.launcher.service.FMCLoginService;
import cn.forevermc.launcher.service.GameService;
import cn.forevermc.launcher.service.PageService;
import cn.forevermc.launcher.util.Path;
import cn.forevermc.launcher.util.Resources;
import com.google.gson.Gson;
import com.imyeyu.compress.CompressType;
import com.imyeyu.compress.Decompressor;
import com.imyeyu.fx.task.RunAsync;
import com.imyeyu.fx.ui.TimiFXUI;
import com.imyeyu.inject.annotation.Component;
import com.imyeyu.inject.annotation.Inject;
import com.imyeyu.io.IO;
import com.imyeyu.io.IOSize;
import com.imyeyu.io.IOSpeedService;
import com.imyeyu.io.JarReader;
import com.imyeyu.java.TimiJava;
import com.imyeyu.java.bean.CallbackArg;
import com.imyeyu.java.bean.CallbackArgReturn;
import com.imyeyu.java.bean.CallbackReturn;
import com.imyeyu.java.bean.timi.TimiCode;
import com.imyeyu.java.bean.timi.TimiException;
import com.imyeyu.network.FileRequest;
import com.imyeyu.network.Network;
import com.imyeyu.network.ProgressiveRequest;
import com.imyeyu.network.TimiRequest;
import com.imyeyu.utils.Digest;
import com.imyeyu.utils.OS;
import com.imyeyu.utils.StringInterpolator;
import com.imyeyu.utils.Text;
import com.imyeyu.utils.Time;
import javafx.application.Platform;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.scene.paint.Paint;
import javafx.stage.Stage;
import lombok.Cleanup;
import lombok.extern.slf4j.Slf4j;
import org.apache.hc.client5.http.fluent.Request;

import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;

/**
 * 启动器
 *
 * @author 夜雨
 * @since 2022-11-21 13:20
 */
@Slf4j
@Component
public class Launcher implements OS.FileSystem {

	/**
	 * 状态，目前仅内部使用
	 *
	 * @author 夜雨
	 * @since 2023-06-16 15:29
	 */
	private enum Status {

		/** 空闲 */
		IDLE,

		/** 初始化 */
		INITIALIZING,

		/** 基础检查 */
		BASE_CHECKING,

		/** 解析版本包 */
		PARSING_PACK,

		/** 账号检查 */
		ACCOUNT_AUTH,

		/** 启动玩家选择 */
		PLAYER_SELECT,

		/** 资源下载 */
		RESOURCE_DOWNLOAD,

		/** Java 下载 */
		JAVA_DOWNLOAD,

		/** JNA 检查 */
		JNA_CHECK,

		/** 构建启动命令 */
		BUILD_COMMAND,

		/** 执行启动 */
		LAUNCHING,

		/** 等待游戏 */
		WAITING_GAME,

		/** 超时 */
		TIMEOUT,

		/** 中断 */
		INTERRUPT,

		/** 成功 */
		SUCCEED
	}

	@Inject
	private Config config;

	@Inject
	private Gson gson;

	@Inject
	private Stage stage;

	@Inject
	private Dialog dialog;

	@Inject
	private GameOption gameOption;

	@Inject
	private LaunchGame launchGame;

	@Inject
	private GameService gameService;

	@Inject
	private PageService pageService;

	@Inject
	private PlayerSelect playerSelect;

	@Inject
	private CanvasProgress canvasProgress;

	@Inject
	private FMCLoginService fmcLoginService;

	@Inject
	private ResourceDownload resourceDownload;

	@Inject
	private APISettingService apiSettingService;

	@Inject
	private IOSpeedService.Item ioSpeedItem;

	private final ObjectProperty<Status> status;

	/** 线程锁 */
	private final Object locker;

	public Launcher() {
		status = new SimpleObjectProperty<>(Status.IDLE);
		locker = new Object();

		// 根据状态切换页面
		status.addListener((obs, o, newStatus) -> {
			if (newStatus != null) {
				switch (newStatus) {
					case IDLE -> pageService.to(Page.MENU);
					case INITIALIZING -> {
						dialog.setContent(TimiFXUI.MULTILINGUAL.text("loading"));
						dialog.eject();
					}
					case ACCOUNT_AUTH -> dialog.setContent(TimiFXUI.MULTILINGUAL.text("fmc.launcher.account.checking"));
					case PLAYER_SELECT -> pageService.to(Page.PLAYER_SELECT);
					case RESOURCE_DOWNLOAD -> pageService.to(Page.RESOURCE_DOWNLOAD);
					case JAVA_DOWNLOAD -> {
						RuntimeDownloadSource source = config.getMain().getRuntimeDownloadSource().get();
						String requiredRTVer = gameService.getRequiredRTVer(launchGame.get());
						log.info("required jre runtime ver.{} and download from {}", source, requiredRTVer);
						canvasProgress.setTask(new RunAsync<String>() {

							/** 镜像地址 */
							static final String MIRROR_API = Resources.TIMI_SERVER_API + "/mirror/%s";

							@Override
							protected String call() throws Exception {
								progress(0);
								OpenJDK jre = getDownloadJRE(source, requiredRTVer);
								String url = getDownloadURL(source, jre);
								long jreLength = FileRequest.wrap(Request.head(url)).length();
								String fileName = "jre" + jre.getVersion() + "." + Network.uriFileExtension(jre.getName());
								File compressedFile = new File("etc" + SEP + fileName);
								if (!compressedFile.exists() || compressedFile.length() != jreLength) {
									log.info("downloading jre: {}", url);
									update("DOWNLOAD");
									ProgressiveRequest.get(url, (total, read, now) -> {
										ioSpeedItem.push(now);
										progress(1D * read / jreLength);
										return !isInterrupted;
									}).toFile(compressedFile);
								}
								if (!isInterrupted && compressedFile.exists()) {
									update("DECOMPRESS");
									log.info("decompressing jre: {}", compressedFile.getAbsolutePath());
									{
										File jreDir = IO.dir("jre" + jre.getVersion());
										Decompressor decompressor = CompressType.fromFile(compressedFile);
										decompressor.setFileCallback(file -> message(file.getName()));
										decompressor.setProgressCallback(this::progress);
										decompressor.run(compressedFile, jreDir.getAbsolutePath());
										IO.destroy(compressedFile);

										File[] jreFiles = jreDir.listFiles();
										if (jreFiles != null && jreFiles.length == 1) {
											// 存在根文件夹
											File[] files = jreFiles[0].listFiles();
											if (TimiJava.isEmpty(files)) {
												throw new TimiException(TimiCode.ERROR, "TODO empty jre files");
											}
											for (int i = 0; i < files.length; i++) {
												IO.move(files[i], jreDir.getAbsolutePath());
											}
											IO.destroy(jreFiles[0]);
										}
									}
									log.info("decompress done");
								}
								return null;
							}

							@Override
							protected void onUpdate(String status) {
								if (TimiJava.isNotEmpty(status)) {
									CallbackArg<Double> dlSpeed = d -> Platform.runLater(() -> canvasProgress.getSubLabel().setText(IOSize.format(d, IOSize.Unit.MB) + "/s"));
									switch (status) {
										case "DOWNLOAD" -> {
											ioSpeedItem.addBufferListener(dlSpeed);

											canvasProgress.setCanCancel(true);
											canvasProgress.setBackgroundColor(TimiFXUI.Colorful.BLACK);
											canvasProgress.setColor(Paint.valueOf("#AAA"));
											canvasProgress.reset();
										}
										case "DECOMPRESS" -> {
											ioSpeedItem.removeBufferListener(dlSpeed);

											canvasProgress.setCanCancel(false);
											canvasProgress.setBackgroundColor(Paint.valueOf("#AAA"));
											canvasProgress.setColor(Paint.valueOf("#0A0"));
											canvasProgress.reset();
										}
									}
								}
							}

							@Override
							protected void onException(Throwable e) {
								// TODO 失败处理
							}

							@Override
							protected void onFinally() {
								// 释放线程
								log.info("runtime download finished and unlocked launcher thread");
								synchronized (locker) {
									locker.notifyAll();
								}
							}

							/**
							 * 获取下载链接
							 *
							 * @param source 下载源
							 * @param jreVer 需求运行时版本
							 * @return 下载链接
							 * @throws Exception 获取异常
							 */
							private OpenJDK getDownloadJRE(RuntimeDownloadSource source, String jreVer) throws Exception {
								List<OpenJDK> jdkList = TimiRequest.<List<OpenJDK>>get(MIRROR_API.formatted(source.getMirror())).result();
								for (int i = 0; i < jdkList.size(); i++) {
									OpenJDK jdk = jdkList.get(i);
									if (jdk.getPlatform() == OS.PLATFORM && jdk.getType() == OpenJDK.Type.JRE && jdk.getVersion().equals(jreVer)) {
										return jdk;
									}
								}
								throw new TimiException(TimiCode.ERROR).msgKey("fmc.launcher.rt.not_support");
							}

							private String getDownloadURL(RuntimeDownloadSource source, OpenJDK jre) {
								return switch (source) {
									case FOREVER_MC -> Resources.TIMI_SERVER_API + "/attachment/download/" + jre.getData();
									case TUNA, GITHUB -> jre.getData();
								};
							}
						});
						canvasProgress.setCanCancel(true);
						canvasProgress.setBackgroundColor(TimiFXUI.Colorful.BLACK);
						canvasProgress.setColor(Paint.valueOf("#AAA"));
						canvasProgress.getLabel().setText(TimiFXUI.MULTILINGUAL.text("fmc.launcher.rt.download.tips", source.getName()));
						pageService.to(Page.CANVAS_PROGRESS);
					}
					case JNA_CHECK -> {
						dialog.setTitle("");
						dialog.setContent(TimiFXUI.MULTILINGUAL.text("fmc.launcher.launching"));
						dialog.eject();
					}
					case WAITING_GAME -> {
						if (launchGame.get().isLegacy()) {
							stage.getIcons().setAll(Resources.ICON_MC_ANCIENT);
						} else {
							stage.getIcons().setAll(Resources.ICON_MC);
						}
						pageService.to(Page.LAUNCHING);
					}
				}
			}
		});
	}

	/** 执行启动 */
	public void launch() {
		if (status.get() != Status.IDLE) {
			throw new TimiException(TimiCode.ERROR_SERVICE_BUSY, "launcher not in idle");
		}
		log.info("initializing launcher..");

		if (status.isBound()) {
			status.unbind();
		}
		status.set(Status.INITIALIZING);
		RunAsync<Status> task = new RunAsync<>() {

			@Override
			protected Status call() throws Exception {
				// 基本数据
				Game game = launchGame.get();

				// 基本校验
				update(Status.BASE_CHECKING);
				if (game == null) {
					log.warn("not select launch game");
					throw new LauncherException(Page.GAME_SELECT, TimiFXUI.MULTILINGUAL.text("fmc.launcher.empty_game"));
				}
				log.info("do launch game: {}", game.getName());

				Game.Option packOption = game.getOption();
				if (TimiJava.isEmpty(config.getPlayer().getName())) {
					log.warn("empty player name");
					throw new LauncherException(Page.MENU, TimiFXUI.MULTILINGUAL.text("fmc.launcher.empty_name"));
				}
				if (launchGame.isOnline() && TimiJava.isEmpty(config.getPlayer().getPassword())) {
					log.warn("empty player password");
					throw new LauncherException(Page.MENU, TimiFXUI.MULTILINGUAL.text("fmc.launcher.empty_password"));
				}

				// 解码版本包
				log.info("parsing game");
				update(Status.PARSING_PACK);
				gameService.readMetaData(game);

				// 验证账号
				String token = null;
				String playerName;
				FMCLoginService.MinecraftPlayer player = null;
				{
					if (launchGame.isOffline()) {
						// 离线
						log.info("skip account check for offline mode");
						playerName = config.getPlayer().getName().get();
					} else {
						fmcLoginService.checkLoginMod(game);
						// 账号验证
						log.info("{}: account checking..", packOption.getLoginType());
						update(Status.ACCOUNT_AUTH);
						String user = config.getPlayer().getName().get();
						String password = config.getPlayer().getPassword().get();

						FMCLoginService.TokenResponse resp = fmcLoginService.genLoginToken(user, password);
						List<FMCLoginService.MinecraftPlayer> playerList = fmcLoginService.listBoundPlayer(resp.getToken());
						if (playerList.isEmpty()) {
							log.warn("not bind minecraft player");
							throw new LauncherException(Page.MENU, "not bind minecraft player");
						}
						if (1 < playerList.size()) {
							FMCLoginService.MinecraftPlayer playerByName = null;
							for (int i = 0; i < playerList.size(); i++) {
								if (playerList.get(i).getName().equals(user)) {
									playerByName = playerList.get(i);
									break;
								}
							}
							if (playerByName != null) {
								player = playerByName;
							} else {
								// 选择启动账号
								playerSelect.getItems().setAll(playerList);
								playerSelect.setOnCloseEvent(() -> {
									synchronized (locker) {
										log.info("selected player and unlocked launcher thread");
										locker.notifyAll();
									}
								});
								update(Status.PLAYER_SELECT);
								// 锁定线程
								log.info("locked launcher thread before selected player");
								synchronized (locker) {
									locker.wait();
								}
								switch (playerSelect.getAction()) {
									case CONFIRM -> player = playerSelect.getSelected();
									case CANCEL -> {
										return Status.INTERRUPT;
									}
								}
							}
						} else {
							// 仅有一个启动账号
							player = playerList.getFirst();
						}
						// 登录
						FMCLoginService.TokenResponse loginResp = fmcLoginService.doLogin(player, resp.getToken());
						playerName = player.getName();
						token = loginResp.getToken();
					}
				}

				// 下载资源
				{
					if (TimiJava.isNotEmpty(game.getMetaData().getDownloadList())) {
						log.info("starting resources download");
						update(Status.RESOURCE_DOWNLOAD);
						resourceDownload.setDownloadDeque(game.getMetaData().getDownloadList());
						resourceDownload.setOnFinallyEvent(() -> {
							log.info("resources download finished and unlocked launcher thread");
							synchronized (locker) {
								locker.notifyAll();
							}
						});
						resourceDownload.start();
						log.info("locked launcher thread before resources download finish");
						// 锁定线程
						synchronized (locker) {
							locker.wait();
						}
					}
				}

				// Java 下载
				File validJRE;
				CallbackReturn<File> installJava = () -> {
					try {
						log.info("finding java..");
						String jreVer = gameService.getRequiredRTVer(game);
						File javaw = new File("jre" + jreVer + SEP + "bin" + SEP + (OS.IS_WINDOWS ? "javaw.exe" : "java"));
						if (javaw.exists() && javaw.isFile() && 0 < javaw.length()) {
							log.info("found at {}", javaw.getAbsolutePath());
							return javaw;
						}
						log.info("fetch jre list from api..");
						// 下载
						update(Status.JAVA_DOWNLOAD);
						// 锁定线程
						log.info("locked launcher thread before runtime download finish");
						synchronized (locker) {
							locker.wait();
						}
						return javaw;
					} catch (Exception e) {
						log.error("download java error", e);
						throw new LauncherException(Page.MENU, TimiFXUI.MULTILINGUAL.text("fmc.launcher.java.error"));
					}
				};
				if (TimiJava.isEmpty(game.getOption().getJava().get())) {
					validJRE = installJava.handler();
				} else {
					File java = new File(game.getOption().getJava().get());
					if (java.exists()) {
						validJRE = java;
					} else {
						validJRE = installJava.handler();
					}
				}

				// 检查 JNA
				update(Status.JNA_CHECK);
				log.info("checking JNA..");
				{
					File natives = IO.dir(Path.P_PACK + game.getMetaData().getVersion() + SEP + "natives");
					final String jnaFormat = OS.IS_WINDOWS ? ".dll" : ".so";
					JarReader jarReader;
					Map<String, InputStream> jarFiles;
					List<Game.MetaData.Libraries> jnaList = game.getMetaData().getLibrarieList().stream().filter(Game.MetaData.Libraries::isNatives).toList();
					for (int i = 0; i < jnaList.size(); i++) {
						// 读取 JNA jar
						File file = new File(Path.P_LIB + jnaList.get(i).getPath());
						jarReader = new JarReader(file);
						jarFiles = jarReader.getFiles();
						// 获取所有文件
						for (Map.Entry<String, InputStream> jarFile : jarFiles.entrySet()) {
							String path = jarFile.getKey();
							if (path.endsWith(jnaFormat)) {
								// 获取 JNA
								String name;
								if (path.contains(SEP)) {
									name = path.substring(path.indexOf("/"));
								} else {
									name = path;
								}
								// 校验 SHA1
								byte[] bytes = jarFile.getValue().readAllBytes();
								String jarSHA1 = Digest.sha1(bytes);
								File jnaFile = new File(natives.getAbsolutePath() + SEP + name);
								if (!jnaFile.exists() || !Digest.sha1(IO.toBytes(jnaFile)).equals(jarSHA1)) {
									// JNA 异常，重新导出
									IO.toFile(jnaFile, new ByteArrayInputStream(bytes));
								}
							}
						}
						jarReader.close();
					}
				}

				// 构建启动命令
				update(Status.BUILD_COMMAND);
				log.info("building launch command..");
				StringBuilder command = new StringBuilder();
				{
					// 依赖 classpath
					String classpath;
					{
						StringBuilder classpathBuilder = new StringBuilder();
						// 一般依赖
						List<Game.MetaData.Libraries> classpathList = game.getMetaData().getLibrarieList().stream().filter(Game.MetaData.Libraries::isNotNatives).distinct().toList();
						for (int i = 0; i < classpathList.size(); i++) {
							classpathBuilder.append(Path.ABS_LIB).append(classpathList.get(i).getPath()).append(';');
						}
						// 上古依赖
						File libDir = new File(IO.fitPath(game.getPath().getAbsolutePath()) + "lib");
						if (libDir.exists() && libDir.isDirectory()) {
							File[] libs = libDir.listFiles();
							if (libs != null) {
								for (int i = 0; i < libs.length; i++) {
									if (libs[i].getName().endsWith(".jar")) {
										classpathBuilder.append(libs[i].getAbsolutePath()).append(';');
									}
								}
							}
						}
						// 启动核心
						classpathBuilder.append(Path.ABS_PACK).append(game.getMetaData().getVersion()).append(SEP).append(game.getMetaData().getVersion()).append(".jar");
						classpath = classpathBuilder.toString();
					}
					CallbackArgReturn<String, String> escapePath = p -> {
						if (p.endsWith("\\")) {
							return Text.quote(p.substring(0, p.length() - 1)).replace("\\", "\\\\");
						} else {
							return Text.quote(p).replace("\\", "\\\\");
						}
					};
					// 启动参数
					String uuid = UUID.randomUUID().toString().replaceAll("-", "");
					Map<String, Object> args = new HashMap<>();
					args.put("classpath", escapePath.handler(classpath));
					args.put("auth_player_name", Text.quote(playerName));
					args.put("version_name", Text.quote(game.getMetaData().getVersion()));
					args.put("game_directory", escapePath.handler(Path.ABS_PACK + game.getName())); // 版本隔离，指向详细版本目录
					args.put("assets_root", escapePath.handler(Path.ABS_ASSETS));
					args.put("game_assets", escapePath.handler(Path.ABS_ASSETS)); // 1.6 以下兼容
					args.put("assets_index_name", game.getMetaData().getAssetsVersion());
					args.put("auth_uuid", uuid);
					args.put("auth_access_token", uuid);
					args.put("auth_session", uuid);  // 1.6 以下兼容
					args.put("clientid", config.getLauncher().getClientId().get());
					args.put("auth_xuid", 0);
					args.put("user_type", "Legacy");
					args.put("version_type", Text.quote("Forever MC"));
					args.put("natives_directory", escapePath.handler(Path.ABS_PACK + game.getMetaData().getVersion() + SEP + "natives"));
					args.put("launcher_name", Text.quote("Forever MC"));
					args.put("launcher_version", ForeverMC.VERSION);
					args.put("quickPlayMultiplayer", Text.quote(game.getOption().getServer().get()));
					// JRE
					command.append('"').append(validJRE.getAbsolutePath()).append('"');
					{
						// 参数插值
						StringInterpolator interpolator = new StringInterpolator(StringInterpolator.DOLLAR_OBJ);
						// 内存参数
						command.append(" -Xmn256m -Xmx").append(config.getLauncher().getMemory().intValue()).append('M');
						if (TimiJava.isNotEmpty(game.getMetaData().getArgsJVM())) {
							// JVM 参数
							command.append(' ').append(interpolator.inject(game.getMetaData().getArgsJVM(), args));
						}
						// 主类
						command.append(' ').append(game.getMetaData().getMainClass());
						// 游戏参数
						command.append(' ').append(interpolator.inject(game.getMetaData().getArgsGame(), args));
					}
					// 令牌
					if (TimiJava.isNotEmpty(token)) {
						command.append(" --token ").append(Text.quote(token));
					}
					// 自动进入服务器（上古版本的方式，新版本会在 quickPlayMultiplayer 参数生效）
					String server = game.getOption().getServer().get();
					if (game.getOption().getAutoConnect().get() && TimiJava.isNotEmpty(server)) {
						command.append(" --server ");
						int addressSplit = server.indexOf(":");
						if (addressSplit == -1) {
							command.append(Text.quote(server)).append(" --port \"25565\"");
						} else {
							String[] address = server.split(":");
							command.append(address[0]).append(" --port ").append(Text.quote(address[1]));
						}
					}
				}

				// 执行启动命令
				update(Status.LAUNCHING);
				StringBuilder infoLog = new StringBuilder(); // 辅助检测启动
				log.info("do launch: {}", command);
				{
					IO.toFile(IO.file("etc" + SEP + "latest-script.bat"), command.toString());
					Runtime runtime = Runtime.getRuntime();
					Process process = runtime.exec(command.toString());
					// 必须把数据流读出来，否则填满缓冲区会导致阻塞，以至超长时间无启动
					new Thread(() -> {
						try {
							@Cleanup InputStream infoIS = process.getInputStream();
							@Cleanup InputStreamReader infoISR = new InputStreamReader(infoIS);
							@Cleanup BufferedReader infoBR = new BufferedReader(infoISR);

							String line;
							infoLog.setLength(0);
							while ((line = infoBR.readLine()) != null) {
								infoLog.append(line).append(System.lineSeparator());
							}
							log.info("process info log: \n{}", infoLog);
						} catch (IOException e) {
							throw new RuntimeException(e);
						}
					}).start();
					new Thread(() -> {
						try {
							@Cleanup InputStream errorIS = process.getErrorStream();
							@Cleanup InputStreamReader errorISR = new InputStreamReader(errorIS);
							@Cleanup BufferedReader errorBR = new BufferedReader(errorISR);

							String line;
							StringBuilder errorLog = new StringBuilder();
							while ((line = errorBR.readLine()) != null) {
								errorLog.append(line).append(System.lineSeparator());
							}
							// TODO 启动失败结束线程
							log.error("process error log: \n{}", errorLog);
						} catch (IOException e) {
							throw new RuntimeException(e);
						}
					}).start();
				}

				// 等待游戏启动
				update(Status.WAITING_GAME);
				{
					long startAt = Time.now();
					String title = "Launching.. " + game.getMetaData().getTitle();
					Platform.runLater(() -> stage.setTitle(title));

					String logFlag = "Setting user: " + playerName;
					if (game.isLegacy()) {
						logFlag = "OpenAL initialized";
					}
					for (int i = 0; i < 64; i++) {
						Thread.sleep(1000);
						// 通过窗体检测
						if (OS.IS_WINDOWS && OS.findProcess4Similarity("javaw.exe", game.getMetaData().getTitle(), title)) {
							log.info("found the game window in {} ms", Time.now() - startAt);
							return Status.SUCCEED;
						}
						// 通过日志检测
						if (infoLog.indexOf(logFlag) != -1) {
							log.info("\"{}\" found in the log indicates successful launch in {} ms", logFlag, Time.now() - startAt);
							return Status.SUCCEED;
						}
					}
				}
				log.warn("launcher timeout, not found window of title: {}", game.getMetaData().getTitle());
				return Status.TIMEOUT;
			}

			@Override
			protected void onFinish(Status status) {
				switch (status) {
					case INTERRUPT -> update(Status.IDLE);
					case SUCCEED -> stage.close();
					case TIMEOUT -> {
						dialog.setContent(TimiFXUI.MULTILINGUAL.text("fmc.launcher.timeout"));
						dialog.setConfirmEvent(() -> pageService.to(Page.MENU));
						dialog.eject();
					}
				}
			}

			@Override
			protected void onException(Throwable e) {
				dialog.setContent(e.getMessage());
				dialog.setCloseEvent(() -> update(Status.IDLE));
				if (e instanceof LauncherException le) {
					log.info("launch exception: {}", e.getMessage());
					dialog.setCloseEvent(() -> {
						update(Status.IDLE);
						pageService.to(le.toPage);
					});
					if (le.toPageEvent != null) {
						le.toPageEvent.handler();
					}
				} else {
					log.info("launch error", e);
				}
				dialog.eject();
			}

			@Override
			protected void onFinally() {
				log.info("launcher on finally");
				synchronized (locker) {
					locker.notifyAll();
				}
			}
		};
		status.bind(task.valueProperty());
		task.messageProperty().addListener((obs, o, newMsg) -> dialog.setContent(newMsg));
		task.start();
	}
}
