package cn.forevermc.launcher.service;

import cn.forevermc.launcher.bean.Config;
import cn.forevermc.launcher.bean.FileDownload;
import cn.forevermc.launcher.bean.Game;
import cn.forevermc.launcher.bean.GameDownloadSource;
import cn.forevermc.launcher.util.Path;
import com.google.gson.Gson;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import com.imyeyu.config.ConfigLoader;
import com.imyeyu.fx.config.BindingsConfig;
import com.imyeyu.fx.ui.TimiFXUI;
import com.imyeyu.inject.annotation.Inject;
import com.imyeyu.inject.annotation.Service;
import com.imyeyu.io.IO;
import com.imyeyu.java.bean.CallbackArgReturn;
import com.imyeyu.java.bean.timi.TimiCode;
import com.imyeyu.java.bean.timi.TimiException;
import com.imyeyu.network.CommonRequest;
import com.imyeyu.network.FileRequest;
import com.imyeyu.network.GsonRequest;
import com.imyeyu.utils.Digest;
import com.imyeyu.utils.OS;
import com.imyeyu.utils.Text;
import com.imyeyu.utils.Time;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;

import java.io.File;
import java.io.IOException;
import java.net.SocketTimeoutException;
import java.security.NoSuchAlgorithmException;
import java.text.ParseException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Pattern;

/**
 * 游戏服务
 *
 * @author 夜雨
 * @since 2022-12-11 19:14
 */
@Slf4j
@Service
public class GameService implements OS.FileSystem {

	/**
	 * JNA 类型
	 *
	 * @author 夜雨
	 * @since 2022-11-29 13:19
	 */
	@AllArgsConstructor
	enum NativeType {

		/** Windows */
		WINDOWS("natives-windows"),

		/** X86 架构 Windows */
		WINDOWS_X86("natives-windows-x86"),

		/** Linux */
		LINUX("natives-linux"),

		/** MacOS */
		MACOS("natives-macos"),

		/** ARM 架构 MacOS */
		MACOS_ARM("natives-macos-arm64");

		/** 位于游戏配置的属性名称 */
		final String name;
	}

	/** 原版启动类 */
	private static final String MAIN_CLASS_MOJANG = "net.minecraft.client.main.Main";

	/** Fabric Loader 启动类 */
	private static final String MAIN_CLASS_FABRIC = "net.fabricmc.loader.impl.launch.knot.KnotClient";

	/** 远古版本资源数据 */
	private static final String ASSETS_ANCIENT = "pre-1.6";

	/** Key: 必须要指定运行时版本的 Minecraft 版本的发布开始时间，此时间点之后的版本需要 Value: Java 版本 */
	private static final Map<Long, String> REQ_RT_MAP = Collections.unmodifiableMap(new LinkedHashMap<>() {{

		try {
			put(Time.dateTimeT.parse("2024-04-03T11:49:39").getTime(), "21"); // 24w14a
			put(Time.dateTimeT.parse("2021-11-16T17:04:48").getTime(), "17"); // 1.18-pre2
			put(0L, "8");
		} catch (ParseException e) {
			throw new RuntimeException(e);
		}

	}});

	/** 配置加载器缓存 */
	private final Map<Game, ConfigLoader<?>> configLoaderMap;

	@Inject
	private Config config;

	@Inject
	private Gson gson;

	public GameService() {
		configLoaderMap = new HashMap<>();
	}

	/** @return 当前版本包列表 */
	public String[] listGames() {
		return new File(Path.P_PACK).list();
	}

	/**
	 * 读取游戏配置
	 *
	 * @param game 版本
	 * @return 游戏配置
	 */
	public Game.Option getOption(Game game) {
		ConfigLoader<Game.Option> loader = BindingsConfig.build("GameOption.yaml", IO.fitPath(game.getPath().getAbsolutePath()) + game.getName() + ".yaml", Game.Option.class);
		configLoaderMap.put(game, loader);
		return loader.load();
	}

	/**
	 * 保存游戏配置
	 *
	 * @param game 游戏
	 */
	public void storeOption(Game game) {
		configLoaderMap.get(game).dump();
	}

	/**
	 * 获取游戏需要运行时最低版本。映射表静态储存 {@link #REQ_RT_MAP}
	 *
	 * @param game 游戏
	 * @return 8、17、21 等 Java 运行时版本
	 */
	public String getRequiredRTVer(Game game) {
		for (Map.Entry<Long, String> item : REQ_RT_MAP.entrySet()) {
			if (item.getKey() < game.getReleaseAt()) {
				return item.getValue();
			}
		}
		throw new TimiException(TimiCode.ERROR).msgKey("TODO get req java version error");
	}

	/**
	 * 构建游戏，解析整合包基本数据，此方法不会深度解析
	 *
	 * @param gamePath 整合包路径文件，通常是 .minecraft/versions 路径的文件夹
	 * @return 版本包数据
	 * @throws IOException 读取异常
	 */
	public Game buildGame(File gamePath) throws Exception {
		File jar = new File(IO.fitPath(gamePath.getAbsolutePath()) + gamePath.getName() + ".jar");
		File json = new File(IO.fitPath(gamePath.getAbsolutePath()) + gamePath.getName() + ".json");
		File yaml = new File(IO.fitPath(gamePath.getAbsolutePath()) + gamePath.getName() + ".yaml");
		Game game = new Game();
		game.setName(gamePath.getName());
		game.setPath(gamePath);
		game.setJar(jar);
		game.setJson(json);
		game.setYaml(yaml);
		JsonObject root = JsonParser.parseString(IO.toString(json)).getAsJsonObject();
		String mainClass = root.get("mainClass").getAsString();

		game.setJsonRoot(root);
		game.setOriginal(MAIN_CLASS_MOJANG.equals(mainClass));
		game.setFabric(MAIN_CLASS_FABRIC.equals(mainClass));
		game.setLegacy(ASSETS_ANCIENT.equals(root.get("assets").getAsString()));
		game.setOption(getOption(game));
		game.setReleaseAt(Time.dateTimeT.parse(root.get("releaseTime").getAsString()).getTime());
		return game;
	}

	/**
	 * 读取版本包元数据，深度解析所有属性，并确定缺少的依赖和资源，比较耗时
	 *
	 * @param game 版本包
	 * @throws Exception 读取异常
	 */
	public void readMetaData(Game game) throws Exception {
		Game.MetaData metaData = new Game.MetaData();
		JsonObject root = game.getJsonRoot();

		// 主版本
		metaData.setVersion(root.get("id").getAsString());
		// 真实版本
		metaData.setRealVersion(getRealVersion(game));
		// 启动核心
		JsonObject core = root.get("downloads").getAsJsonObject().get("client").getAsJsonObject();
		metaData.setCoreSHA1(core.get("sha1").getAsString());
		metaData.setCoreURL(core.get("url").getAsString());
		metaData.setCoreSize(core.get("size").getAsLong());
		// 资源文件
		log.info("parsing assets list..");
		metaData.setAssetsVersion(root.get("assets").getAsString());
		metaData.setAssetList(parseAssetsList(root.get("assetIndex").getAsJsonObject().get("url").getAsString(), metaData));
		// 依赖文件
		// TODO Create Astral 长时间卡这里
		log.info("parsing libraries list..");
		metaData.setLibrarieList(parseLibrariesList(root.get("libraries").getAsJsonArray()));
		// 启动类
		metaData.setMainClass(root.get("mainClass").getAsString());
		// Java 版本
		metaData.setJavaVersion(root.get("javaVersion").getAsJsonObject().get("majorVersion").getAsString());
		{
			// 启动参数模板
			log.info("generate arguments template for launch");
			if (root.has("arguments")) {
				JsonObject arguments = root.get("arguments").getAsJsonObject();
				if (arguments.has("game")) {
					metaData.setArgsGame(parseArgsArray(game, arguments.get("game").getAsJsonArray()));
				}
				if (arguments.has("jvm")) {
					metaData.setArgsJVM(parseArgsArray(game, arguments.get("jvm").getAsJsonArray()));
				}
			} else if (root.has("minecraftArguments")) {
				// 远古版本
				metaData.setArgsGame(root.get("minecraftArguments").getAsString());
				metaData.setArgsJVM(" -Dminecraft.applet.TargetDirectory=${game_directory} -Djava.library.path=${natives_directory} -cp ${classpath}");
			}
		}
		// 标题
		if (root.has("title")) {
			metaData.setTitle(root.get("title").getAsString());
		} else if (root.has("id")) {
			metaData.setTitle("Minecraft " + root.get("id").getAsString());
		}
		if (game.isLegacy()) {
			metaData.setTitle("Minecraft");
		}
		log.info("parsing required download file list");
		// 解析需要下载的资源
		metaData.setDownloadList(toFileDL(metaData));
		if (metaData.getDownloadList().isEmpty()) {
			log.info("nothing required download");
		} else {
			log.info("required download size {}", metaData.getDownloadList().size());
		}
		// 设置元数据
		game.setMetaData(metaData);
		// JSON 资源可能被补充 size 或 sha1，写入磁盘
		IO.toFile(game.getJson(), gson.toJson(root));
	}

	/**
	 * 获取版本包的真实游戏版本
	 *
	 * @param game 版本包
	 * @return 真实游戏版本
	 */
	public Game.MetaData.RealVersion getRealVersion(Game game) {
		JsonObject root = game.getJsonRoot();
		// 真实版本
		Game.MetaData.RealVersion realVersion = new Game.MetaData.RealVersion();
		String value = root.get("id").getAsString();
		if (root.has("source-id")) {
			value = root.get("source-id").getAsString();
		}
		// HMCL 的原始版本
		if (root.has("patches")) {
			JsonArray patches = root.get("patches").getAsJsonArray();
			for (int j = 0; j < patches.size(); j++) {
				JsonObject object = patches.get(j).getAsJsonObject();
				if (object.has("id") && object.get("id").getAsString().equals("game")) {
					value = object.get("version").getAsString();
					break;
				}
			}
		}
		realVersion.setValue(value);
		return realVersion;
	}

	/**
	 * 解析版本需要下载的依赖和资源
	 *
	 * @param metadata 版本数据
	 * @return 依赖和资源文件
	 */
	private List<FileDownload> toFileDL(Game.MetaData metadata) throws IOException, NoSuchAlgorithmException {
		List<FileDownload> dlList = new ArrayList<>();

		File file;
		FileDownload fileDL;
		String version = metadata.getVersion();

		// TODO 告知 UI 正在检查项

		// 核心
		{
			file = new File(Path.P_PACK + version + SEP + version + ".jar");
			if (!file.exists() || file.length() == 0 || file.length() != metadata.getCoreSize() || !Digest.sha1(IO.toBytes(file)).equals(metadata.getCoreSHA1())) {
				fileDL = new FileDownload(file);
				fileDL.setHash(metadata.getCoreSHA1());
				fileDL.setUrl(metadata.getCoreURL());
				fileDL.setSize(metadata.getCoreSize());
				dlList.add(fileDL);
			}
		}
		// 资源
		String assetsPath = Path.P_ASSETS + "objects" + SEP;
		List<Game.MetaData.Assets> assetList = metadata.getAssetList();
		for (int i = 0; i < assetList.size(); i++) {
			Game.MetaData.Assets asset = assetList.get(i);
			file = new File(assetsPath + asset.getHash().substring(0, 2) + SEP + asset.getHash());
			if (!file.exists() || file.length() == 0 || file.length() != asset.getSize() || !Digest.sha1(IO.toBytes(file)).equals(asset.getHash())) {
				fileDL = new FileDownload(file);
				fileDL.setHash(asset.getHash());
				fileDL.setUrl(asset.getUrl());
				fileDL.setSize(asset.getSize());
				dlList.add(fileDL);
			}
		}

		// 依赖
		List<Game.MetaData.Libraries> librarieList = metadata.getLibrarieList();
		for (int i = 0; i < librarieList.size(); i++) {
			Game.MetaData.Libraries library = librarieList.get(i);
			String path = library.getPath();

			file = new File(Path.P_LIB + path.substring(0, path.lastIndexOf("/")) + path.substring(path.lastIndexOf("/")));
			if (!file.exists() || file.length() == 0 || file.length() != library.getSize() || !Digest.sha1(IO.toBytes(file)).equals(library.getSha1())) {
				fileDL = new FileDownload(file);
				fileDL.setHash(library.getSha1());
				fileDL.setUrl(library.getUrl());
				fileDL.setSize(library.getSize());
				dlList.add(fileDL);
			}
		}

		return dlList;
	}

	/**
	 * 解析 JSON 配置启动参数
	 *
	 * @param array 启动参数 JSON
	 * @return 启动参数命令
	 */
	private String parseArgsArray(Game game, JsonArray array) {
		StringBuilder sb = new StringBuilder();

		String osVersion = System.getProperty("os.version");

		CallbackArgReturn<String, String> qmArgs = p -> {
			int i = p.indexOf("=");
			if (i == -1) {
				return p;
			} else {
				if (p.charAt(i + 1) == '$') {
					return p;
				} else {
					return p.substring(0, i + 1) + Text.quote(p.substring(i + 1));
				}
			}
		};

		JsonObject arg, rule, os;
		JsonArray rules, values;
		args: for (int i = 0; i < array.size(); i++) {
			if (array.get(i).isJsonObject()) {
				arg = array.get(i).getAsJsonObject();
				rules = arg.get("rules").getAsJsonArray();
				if (arg.get("value").isJsonArray()) {
					// 数组参数
					for (int j = 0; j < rules.size(); j++) {
						// 约束规则
						rule = rules.get(j).getAsJsonObject();
						if (rule.get("action").getAsString().equals("allow")) {
							// 允许的规则
							if (rule.has("os")) {
								// 系统约束
								os = rule.get("os").getAsJsonObject();
								if (os.has("name")) {
									if (OS.IS_WINDOWS && os.get("name").getAsString().equals("windows")) {
										// windows 系统
										if (os.has("version")) {
											// 版本约束
											if (!Pattern.matches(os.get("version").getAsString(), osVersion.substring(0, osVersion.length() - 1))) {
												// 版本不通过
												continue args;
											}
										}
										if (arg.get("value").isJsonArray()) {
											// 多值
											values = arg.get("value").getAsJsonArray();
											for (int k = 0; k < values.size(); k++) {
												sb.append(qmArgs.handler(values.get(k).getAsString())).append(' ');
											}
										} else {
											// 单值
											sb.append(qmArgs.handler(arg.get("value").getAsString())).append(' ');
										}
										continue args;
									} // else Linux 系统
								} // else 非系统名称约束
							} else if (rule.has("features")) {
								// 可选规则（暂不支持自定义规则）
//								for (int k = 0; k < optionRules.size(); k++) {
//									if (rule.get("features").getAsJsonObject().has(optionRules.get(k))) {
//										if (arg.get("value").isJsonArray()) {
//											// 多值
//											values = arg.get("value").getAsJsonArray();
//											for (int m = 0; m < values.size(); m++) {
//												sb.append(qmArgs.handler(values.get(m).getAsString())).append(' ');
//											}
//										} else {
//											// 单值
//											sb.append(qmArgs.handler(arg.get("value").getAsString())).append(' ');
//										}
//										break;
//									}
//								}
							}
						} // else 禁用的规则
					}
				} // 非数组参数
			} else {
				// 通用字符串参数
				sb.append(array.get(i).getAsString()).append(' ');
			}
		}
		return sb.toString();
	}

	/**
	 * 解析资源列表
	 *
	 * @param assetsURL 资源数据 JSON URL
	 * @return 资源数据
	 * @throws Exception 异常
	 */
	private List<Game.MetaData.Assets> parseAssetsList(String assetsURL, Game.MetaData metaData) throws Exception {
		List<Game.MetaData.Assets> list = new ArrayList<>();

		final String urlDomain = config.getMain().getGameDownloadSource().get().getResources();

		String indexJson = null;
		int retry = 0;
		while (retry < 10) {
			try {
				indexJson = CommonRequest.get(assetsURL).asString();
				break;
			} catch (SocketTimeoutException e) {
				retry++;
				log.error("official resources api error", e);
			}
		}
		if (indexJson == null) {
			throw new TimiException(TimiCode.ERROR, TimiFXUI.MULTILINGUAL.text("fmc.launcher.resource_error"));
		}
		IO.toFile(new File(Path.P_ASSETS + "indexes" + SEP + metaData.getAssetsVersion() + ".json"), indexJson);

		JsonObject objects = GsonRequest.get(assetsURL).asJsonObject().get("objects").getAsJsonObject();

		Game.MetaData.Assets assets;
		for (Map.Entry<String, JsonElement> object : objects.entrySet()) {
			if (object.getValue() instanceof JsonObject jo) {
				assets = new Game.MetaData.Assets();
				assets.setHash(jo.get("hash").getAsString());
				assets.setUrl(urlDomain + assets.getHash().substring(0, 2) + '/' + assets.getHash());
				assets.setSize(jo.get("size").getAsLong());

				list.add(assets);
			}
		}
		return list;
	}

	/**
	 * 解析依赖
	 *
	 * @param libs 依赖列表
	 * @return 依赖
	 */
	private List<Game.MetaData.Libraries> parseLibrariesList(JsonArray libs) throws Exception {
		List<Game.MetaData.Libraries> list = new ArrayList<>();

		Game.MetaData.Libraries libraries;
		JsonObject lib, rule, os, downloads, artifact, natives, classifiers, classifier;
		JsonArray rules;

		libs:
		for (int i = 0; i < libs.size(); i++) {
			lib = libs.get(i).getAsJsonObject();

			if (lib.has("name") && lib.has("url")) {
				// 兼容 HMCL 的 JSON 结构仅有 name 和 url，临时转为本启动器结构
				String name = lib.get("name").getAsString();
				String[] names = name.split(":");

				// 合成路径
				StringBuilder pathSB = new StringBuilder();
				String[] paths = names[0].split("\\.");
				for (int j = 0; j < paths.length; j++) {
					pathSB.append(paths[j]).append('/');
				}
				pathSB.append(names[1]).append('/').append(names[2]).append('/').append(names[1]).append('-').append(names[2]).append(".jar");

				downloads = new JsonObject();
				{
					String path = pathSB.toString();
					String url = lib.get("url").getAsString() + path;
					artifact = new JsonObject();
					artifact.addProperty("path", path);
					artifact.addProperty("url", url);
					artifact.addProperty("size", FileRequest.get(url).length());
					artifact.addProperty("sha1", Digest.sha1(FileRequest.get(url).asBytes()));

					downloads.add("artifact", artifact);

					lib.add("downloads", downloads);
				}
			}
			if (lib.has("downloads")) {
				downloads = lib.get("downloads").getAsJsonObject();
				if (lib.has("rules")) {
					// 约束规则
					rules = lib.get("rules").getAsJsonArray();
					for (int j = 0; j < rules.size(); j++) {
						rule = rules.get(j).getAsJsonObject();
						switch (rule.get("action").getAsString()) {
							case "allow" -> {
								if (rule.has("os")) {
									os = rule.get("os").getAsJsonObject();
									if (os.has("name") && os.get("name").getAsString().equals("osx") && !OS.IS_OSX) {
										// OSX 专用
										continue libs;
									}
								} // else 通用
							}
							case "disallow" -> {
								if (rule.has("os")) {
									os = rule.get("os").getAsJsonObject();
									if (os.has("name") && os.get("name").getAsString().equals("osx") && OS.IS_OSX) {
										// OSX 禁用
										continue libs;
									}
								} // else 通用
							}
						}
					}
				}
				// JNA
				if (lib.has("natives")) {
					// 1.18 及以下
					natives = lib.get("natives").getAsJsonObject();
					// Windows JNA
					if (natives.has("windows") && OS.IS_WINDOWS) {
						classifiers = downloads.get("classifiers").getAsJsonObject();
						classifier = classifiers.get(natives.get("windows").getAsString()).getAsJsonObject();

						libraries = new Game.MetaData.Libraries();
						libraries.setNatives(true);
						libraries.setPath(classifier.get("path").getAsString());
						libraries.setSha1(classifier.get("sha1").getAsString());
						libraries.setSize(classifier.get("size").getAsLong());
						libraries.setUrl(classifier.get("url").getAsString());

						list.add(libraries);
					}
					// 其他系统 JNA
				}

				if (downloads.has("artifact")) {
					artifact = downloads.get("artifact").getAsJsonObject();

					libraries = new Game.MetaData.Libraries();
					String name = lib.get("name").getAsString();
					if (OS.IS_WINDOWS) {
						libraries.setNatives(name.endsWith("natives-windows"));
					}
					if (OS.IS_UNIX) {
						libraries.setNatives(name.endsWith("natives-linux"));
					}
					if (OS.IS_OSX) {
						libraries.setNatives(name.endsWith("natives-macos"));
					}
					libraries.setPath(artifact.get("path").getAsString());

					// TODO 告知 UI 耗时操作
					String url = artifact.get("url").getAsString();
					if (artifact.has("sha1")) {
						libraries.setSha1(artifact.get("sha1").getAsString());
					} else {
						String sha1 = Digest.sha1(FileRequest.get(url).asBytes());
						artifact.addProperty("sha1", sha1);
						libraries.setSha1(sha1);
					}
					if (artifact.has("size")) {
						libraries.setSize(artifact.get("size").getAsLong());
					} else {
						long size = FileRequest.get(url).length();
						artifact.addProperty("size", size);
						libraries.setSize(size);
					}
					libraries.setUrl(url);
					list.add(libraries);
				}
			}
		}
		// 替换下载源
		GameDownloadSource source = config.getMain().getGameDownloadSource().get();
		for (int i = 0; i < list.size(); i++) {
			String fromURL = list.get(i).getUrl();
			String toURL = fromURL.replaceAll(GameDownloadSource.MOJANG.getLibraries(), source.getLibraries());
			list.get(i).setUrl(toURL);
		}
		return list;
	}
}
