package mezz.jei.fabric.test;

import mezz.jei.api.constants.ModIds;
import mezz.jei.test.client.ExternalServerClient;
import mezz.jei.test.lib.ExternalServerProcess;
import net.fabricmc.fabric.api.client.gametest.v1.context.ClientGameTestContext;
import net.fabricmc.loader.api.FabricLoader;
import net.fabricmc.loader.api.ModContainer;
import net.minecraft.client.Minecraft;

import java.io.File;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.function.Supplier;
import java.util.stream.Collectors;

/**
 * Starts disposable vanilla or Fabric dedicated servers for client connection tests.
 */
@SuppressWarnings("UnstableApiUsage")
final class ExternalTestServer implements AutoCloseable {
	private final ExternalServerClient.ClientAccess clientAccess;
	private final ExternalServerProcess server;

	private ExternalTestServer(ExternalServerClient.ClientAccess clientAccess, ExternalServerProcess server) {
		this.clientAccess = clientAccess;
		this.server = server;
	}

	public static ExternalTestServer startVanilla(ClientGameTestContext context) {
		return start(context, "vanilla-server-without-jei", LaunchType.VANILLA);
	}

	public static ExternalTestServer startFabricWithJei(ClientGameTestContext context) {
		return start(context, "fabric-server-with-jei", LaunchType.FABRIC_WITH_JEI);
	}

	public static ExternalTestServer startFabricWithoutJei(ClientGameTestContext context) {
		return start(context, "fabric-server-without-jei", LaunchType.FABRIC_WITHOUT_JEI);
	}

	private static ExternalTestServer start(ClientGameTestContext context, String directoryName, LaunchType launchType) {
		ExternalServerClient.ClientAccess clientAccess = createClientAccess(context);
		ExternalServerClient.assertNativeTransportDisabled(clientAccess);
		ExternalServerProcess server = ExternalServerProcess.start(directoryName, launchType.description, launchType);
		return new ExternalTestServer(clientAccess, server);
	}

	public Connection connect() {
		ExternalServerClient.connect(server, clientAccess);
		return new Connection(clientAccess);
	}

	private static ExternalServerClient.ClientAccess createClientAccess(ClientGameTestContext context) {
		return new ExternalServerClient.ClientAccess() {
			@Override
			public void run(Consumer<Minecraft> task) {
				context.runOnClient(task::accept);
			}

			@Override
			public <T> T compute(Function<Minecraft, T> task) {
				return context.computeOnClient(task::apply);
			}

			@Override
			public void waitFor(Predicate<Minecraft> predicate, Supplier<String> timeoutMessage) {
				try {
					context.waitFor(predicate::test, ExternalServerClient.EXTERNAL_SERVER_CLIENT_TIMEOUT_TICKS);
				} catch (AssertionError e) {
					throw new AssertionError(timeoutMessage.get(), e);
				}
			}
		};
	}

	@Override
	public void close() {
		server.close();
	}

	private enum LaunchType implements ExternalServerProcess.Launcher {
		FABRIC_WITH_JEI("Fabric server with JEI", "net.fabricmc.loader.impl.launch.server.FabricServerLauncher") {
			@Override
			protected void addJvmArguments(List<String> command, int port) {
				command.add("-Dfabric.development=true");
			}

			@Override
			protected String classpath(Path serverDirectory) {
				return ExternalServerProcess.currentClasspath();
			}
		},
		FABRIC_WITHOUT_JEI("Fabric server without JEI", "net.fabricmc.loader.impl.launch.server.FabricServerLauncher") {
			@Override
			protected void addJvmArguments(List<String> command, int port) {
				command.add("-Dfabric.development=true");
			}

			@Override
			protected String classpath(Path serverDirectory) {
				return currentClasspathWithoutJei();
			}
		},
		VANILLA("vanilla server without JEI", "net.minecraft.server.Main") {
			@Override
			protected String classpath(Path serverDirectory) {
				return ExternalServerProcess.currentClasspath();
			}
		};

		private static final List<String> SERVER_WITHOUT_JEI_EXCLUDED_MOD_IDS = List.of(ModIds.JEI_ID, "jei-test");

		private final String description;
		private final String mainClass;

		LaunchType(String description, String mainClass) {
			this.description = description;
			this.mainClass = mainClass;
		}

		@Override
		public List<String> createCommand(Path serverDirectory, int port) {
			List<String> command = new ArrayList<>();
			command.add(ExternalServerProcess.javaBinary());
			command.add("-Xmx1G");
			addJvmArguments(command, port);
			command.add("-cp");
			command.add(classpath(serverDirectory));
			command.add(mainClass);
			command.add("nogui");
			return command;
		}

		protected void addJvmArguments(List<String> command, int port) {

		}

		protected abstract String classpath(Path serverDirectory);

		private static String currentClasspathWithoutJei() {
			Set<Path> excludedModRoots = getModRootPaths(SERVER_WITHOUT_JEI_EXCLUDED_MOD_IDS);
			return Arrays.stream(ExternalServerProcess.currentClasspath().split(File.pathSeparator))
				.filter(path -> !isExcludedModRoot(path, excludedModRoots))
				.collect(Collectors.joining(File.pathSeparator));
		}

		private static Set<Path> getModRootPaths(Collection<String> modIds) {
			FabricLoader loader = FabricLoader.getInstance();
			return modIds.stream()
				.map(loader::getModContainer)
				.flatMap(Optional::stream)
				.map(ModContainer::getRootPaths)
				.flatMap(Collection::stream)
				.map(LaunchType::normalizePath)
				.collect(Collectors.toUnmodifiableSet());
		}

		private static boolean isExcludedModRoot(String classpathEntry, Set<Path> excludedModRoots) {
			return excludedModRoots.contains(normalizePath(classpathEntry));
		}

		private static Path normalizePath(String path) {
			return normalizePath(Path.of(path));
		}

		private static Path normalizePath(Path path) {
			return path.toAbsolutePath().normalize();
		}
	}

	static final class Connection implements AutoCloseable {
		private final ExternalServerClient.ClientAccess clientAccess;

		private Connection(ExternalServerClient.ClientAccess clientAccess) {
			this.clientAccess = clientAccess;
		}

		@Override
		public void close() {
			ExternalServerClient.disconnect(clientAccess);
		}
	}
}
