package net.minecraft.server.integrated;

import com.mojang.datafixers.util.Pair;
import com.mojang.logging.LogUtils;
import com.mojang.serialization.DynamicOps;
import com.mojang.serialization.Lifecycle;
import it.unimi.dsi.fastutil.booleans.BooleanConsumer;
import java.io.IOException;
import java.util.concurrent.CompletableFuture;
import java.util.function.Function;
import net.fabricmc.api.EnvType;
import net.fabricmc.api.Environment;
import net.minecraft.client.MinecraftClient;
import net.minecraft.client.gui.screen.BackupPromptScreen;
import net.minecraft.client.gui.screen.ConfirmScreen;
import net.minecraft.client.gui.screen.DatapackFailureScreen;
import net.minecraft.client.gui.screen.NoticeScreen;
import net.minecraft.client.gui.screen.Screen;
import net.minecraft.client.gui.screen.world.CreateWorldScreen;
import net.minecraft.client.gui.screen.world.EditWorldScreen;
import net.minecraft.client.gui.screen.world.SymlinkWarningScreen;
import net.minecraft.client.toast.SystemToast;
import net.minecraft.client.world.GeneratorOptionsHolder;
import net.minecraft.nbt.NbtElement;
import net.minecraft.nbt.NbtOps;
import net.minecraft.registry.CombinedDynamicRegistries;
import net.minecraft.registry.DynamicRegistryManager;
import net.minecraft.registry.Registry;
import net.minecraft.registry.RegistryKeys;
import net.minecraft.registry.RegistryOps;
import net.minecraft.registry.ServerDynamicRegistryType;
import net.minecraft.registry.SimpleRegistry;
import net.minecraft.resource.DataConfiguration;
import net.minecraft.resource.LifecycledResourceManager;
import net.minecraft.resource.ResourcePackManager;
import net.minecraft.resource.VanillaDataPackProvider;
import net.minecraft.screen.ScreenTexts;
import net.minecraft.server.DataPackContents;
import net.minecraft.server.SaveLoader;
import net.minecraft.server.SaveLoading;
import net.minecraft.server.SaveLoading.DataPacks;
import net.minecraft.server.SaveLoading.LoadContext;
import net.minecraft.server.SaveLoading.LoadContextSupplier;
import net.minecraft.server.SaveLoading.SaveApplierFactory;
import net.minecraft.server.SaveLoading.ServerConfig;
import net.minecraft.server.command.CommandManager.RegistrationEnvironment;
import net.minecraft.text.Text;
import net.minecraft.util.Util;
import net.minecraft.util.crash.CrashReport;
import net.minecraft.util.path.SymlinkValidationException;
import net.minecraft.world.SaveProperties;
import net.minecraft.world.dimension.DimensionOptions;
import net.minecraft.world.dimension.DimensionOptionsRegistryHolder;
import net.minecraft.world.dimension.DimensionOptionsRegistryHolder.DimensionsConfig;
import net.minecraft.world.gen.GeneratorOptions;
import net.minecraft.world.level.LevelInfo;
import net.minecraft.world.level.LevelProperties;
import net.minecraft.world.level.storage.LevelStorage;
import net.minecraft.world.level.storage.LevelStorage.Session;
import org.jetbrains.annotations.Nullable;
import org.slf4j.Logger;

@Environment(EnvType.CLIENT)
public class IntegratedServerLoader {
	private static final Logger LOGGER = LogUtils.getLogger();
	private final MinecraftClient client;
	private final LevelStorage storage;

	public IntegratedServerLoader(MinecraftClient client, LevelStorage storage) {
		this.client = client;
		this.storage = storage;
	}

	public void start(Screen parent, String levelName) {
		this.start(parent, levelName, false, true);
	}

	public void createAndStart(
		String levelName,
		LevelInfo levelInfo,
		GeneratorOptions dynamicRegistryManager,
		Function<DynamicRegistryManager, DimensionOptionsRegistryHolder> dimensionsRegistrySupplier
	) {
		Session session = this.createSession(levelName);
		if (session != null) {
			ResourcePackManager resourcePackManager = VanillaDataPackProvider.createManager(session);
			DataConfiguration dataConfiguration = levelInfo.getDataConfiguration();

			try {
				DataPacks dataPacks = new DataPacks(resourcePackManager, dataConfiguration, false, false);
				SaveLoader saveLoader = this.load(
					dataPacks,
					context -> {
						DimensionsConfig dimensionsConfig = ((DimensionOptionsRegistryHolder)dimensionsRegistrySupplier.apply(context.worldGenRegistryManager()))
							.toConfig(context.dimensionsRegistryManager().get(RegistryKeys.DIMENSION));
						return new LoadContext(
							new LevelProperties(levelInfo, dynamicRegistryManager, dimensionsConfig.specialWorldProperty(), dimensionsConfig.getLifecycle()),
							dimensionsConfig.toDynamicRegistryManager()
						);
					},
					SaveLoader::new
				);
				this.client.startIntegratedServer(levelName, session, resourcePackManager, saveLoader, true);
			} catch (Exception var10) {
				LOGGER.warn("Failed to load datapacks, can't proceed with server load", (Throwable)var10);
				close(session, levelName);
			}
		}
	}

	@Nullable
	private Session createSession(String levelName) {
		try {
			return this.storage.createSession(levelName);
		} catch (IOException var3) {
			LOGGER.warn("Failed to read level {} data", levelName, var3);
			SystemToast.addWorldAccessFailureToast(this.client, levelName);
			this.client.setScreen(null);
			return null;
		} catch (SymlinkValidationException var4) {
			LOGGER.warn("{}", var4.getMessage());
			this.client.setScreen(new SymlinkWarningScreen(null));
			return null;
		}
	}

	public void start(
		Session session,
		DataPackContents dataPackContents,
		CombinedDynamicRegistries<ServerDynamicRegistryType> dynamicRegistryManager,
		SaveProperties saveProperties
	) {
		ResourcePackManager resourcePackManager = VanillaDataPackProvider.createManager(session);
		LifecycledResourceManager lifecycledResourceManager = (LifecycledResourceManager)new DataPacks(
				resourcePackManager, saveProperties.getDataConfiguration(), false, false
			)
			.load()
			.getSecond();
		this.client
			.startIntegratedServer(
				session.getDirectoryName(),
				session,
				resourcePackManager,
				new SaveLoader(lifecycledResourceManager, dataPackContents, dynamicRegistryManager, saveProperties),
				true
			);
	}

	private SaveLoader createSaveLoader(Session session, boolean safeMode, ResourcePackManager dataPackManager) throws Exception {
		DataPacks dataPacks = this.createDataPackConfig(session, safeMode, dataPackManager);
		return this.load(
			dataPacks,
			context -> {
				DynamicOps<NbtElement> dynamicOps = RegistryOps.of(NbtOps.INSTANCE, context.worldGenRegistryManager());
				Registry<DimensionOptions> registry = context.dimensionsRegistryManager().get(RegistryKeys.DIMENSION);
				Pair<SaveProperties, DimensionsConfig> pair = session.readLevelProperties(
					dynamicOps, context.dataConfiguration(), registry, context.worldGenRegistryManager().getRegistryLifecycle()
				);
				if (pair == null) {
					throw new IllegalStateException("Failed to load world");
				} else {
					return new LoadContext(pair.getFirst(), pair.getSecond().toDynamicRegistryManager());
				}
			},
			SaveLoader::new
		);
	}

	public Pair<LevelInfo, GeneratorOptionsHolder> loadForRecreation(Session session) throws Exception {
		ResourcePackManager resourcePackManager = VanillaDataPackProvider.createManager(session);
		DataPacks dataPacks = this.createDataPackConfig(session, false, resourcePackManager);

		@Environment(EnvType.CLIENT)
		record CurrentSettings(LevelInfo levelInfo, GeneratorOptions options, Registry<DimensionOptions> existingDimensionRegistry) {
		}

		return this.load(
			dataPacks,
			context -> {
				DynamicOps<NbtElement> dynamicOps = RegistryOps.of(NbtOps.INSTANCE, context.worldGenRegistryManager());
				Registry<DimensionOptions> registry = new SimpleRegistry(RegistryKeys.DIMENSION, Lifecycle.stable()).freeze();
				Pair<SaveProperties, DimensionsConfig> pair = session.readLevelProperties(
					dynamicOps, context.dataConfiguration(), registry, context.worldGenRegistryManager().getRegistryLifecycle()
				);
				if (pair == null) {
					throw new IllegalStateException("Failed to load world");
				} else {
					return new LoadContext(
						new CurrentSettings(pair.getFirst().getLevelInfo(), pair.getFirst().getGeneratorOptions(), pair.getSecond().dimensions()),
						context.dimensionsRegistryManager()
					);
				}
			},
			(resourceManager, dataPackContents, combinedRegistryManager, currentSettings) -> {
				resourceManager.close();
				return Pair.of(
					currentSettings.levelInfo,
					new GeneratorOptionsHolder(
						currentSettings.options,
						new DimensionOptionsRegistryHolder(currentSettings.existingDimensionRegistry),
						combinedRegistryManager,
						dataPackContents,
						currentSettings.levelInfo.getDataConfiguration()
					)
				);
			}
		);
	}

	private DataPacks createDataPackConfig(Session session, boolean safeMode, ResourcePackManager dataPackManager) {
		DataConfiguration dataConfiguration = session.getDataPackSettings();
		if (dataConfiguration == null) {
			throw new IllegalStateException("Failed to load data pack config");
		} else {
			return new DataPacks(dataPackManager, dataConfiguration, safeMode, false);
		}
	}

	public SaveLoader createSaveLoader(Session session, boolean safeMode) throws Exception {
		ResourcePackManager resourcePackManager = VanillaDataPackProvider.createManager(session);
		return this.createSaveLoader(session, safeMode, resourcePackManager);
	}

	private <D, R> R load(DataPacks dataPacks, LoadContextSupplier<D> loadContextSupplier, SaveApplierFactory<D, R> saveApplierFactory) throws Exception {
		ServerConfig serverConfig = new ServerConfig(dataPacks, RegistrationEnvironment.INTEGRATED, 2);
		CompletableFuture<R> completableFuture = SaveLoading.load(serverConfig, loadContextSupplier, saveApplierFactory, Util.getMainWorkerExecutor(), this.client);
		this.client.runTasks(completableFuture::isDone);
		return (R)completableFuture.get();
	}

	private void start(Screen parent, String levelName, boolean safeMode, boolean canShowBackupPrompt) {
		Session session = this.createSession(levelName);
		if (session != null) {
			ResourcePackManager resourcePackManager = VanillaDataPackProvider.createManager(session);

			SaveLoader saveLoader;
			try {
				saveLoader = this.createSaveLoader(session, safeMode, resourcePackManager);
			} catch (Exception var11) {
				LOGGER.warn("Failed to load level data or datapacks, can't proceed with server load", (Throwable)var11);
				if (!safeMode) {
					this.client.setScreen(new DatapackFailureScreen(() -> this.start(parent, levelName, true, canShowBackupPrompt)));
				} else {
					this.client
						.setScreen(
							new NoticeScreen(
								() -> this.client.setScreen(null),
								Text.translatable("datapackFailure.safeMode.failed.title"),
								Text.translatable("datapackFailure.safeMode.failed.description"),
								ScreenTexts.TO_TITLE,
								true
							)
						);
				}

				close(session, levelName);
				return;
			}

			SaveProperties saveProperties = saveLoader.saveProperties();
			boolean bl = saveProperties.getGeneratorOptions().isLegacyCustomizedType();
			boolean bl2 = saveProperties.getLifecycle() != Lifecycle.stable();
			if (!canShowBackupPrompt || !bl && !bl2) {
				this.client.getServerResourcePackProvider().loadServerPack(session).thenApply(void_ -> true).exceptionallyComposeAsync(throwable -> {
					LOGGER.warn("Failed to load pack: ", throwable);
					return this.showPackLoadFailureScreen();
				}, this.client).thenAcceptAsync(proceed -> {
					if (proceed) {
						this.client.startIntegratedServer(levelName, session, resourcePackManager, saveLoader, false);
					} else {
						saveLoader.close();
						close(session, levelName);
						this.client.getServerResourcePackProvider().clear().thenRunAsync(() -> this.client.setScreen(parent), this.client);
					}
				}, this.client).exceptionally(throwable -> {
					this.client.setCrashReportSupplierAndAddDetails(CrashReport.create(throwable, "Load world"));
					return null;
				});
			} else {
				this.showBackupPromptScreen(parent, levelName, bl, () -> this.start(parent, levelName, safeMode, false));
				saveLoader.close();
				close(session, levelName);
			}
		}
	}

	private CompletableFuture<Boolean> showPackLoadFailureScreen() {
		CompletableFuture<Boolean> completableFuture = new CompletableFuture();
		this.client
			.setScreen(
				new ConfirmScreen(
					completableFuture::complete,
					Text.translatable("multiplayer.texturePrompt.failure.line1"),
					Text.translatable("multiplayer.texturePrompt.failure.line2"),
					ScreenTexts.PROCEED,
					ScreenTexts.CANCEL
				)
			);
		return completableFuture;
	}

	private static void close(Session session, String levelName) {
		try {
			session.close();
		} catch (IOException var3) {
			LOGGER.warn("Failed to unlock access to level {}", levelName, var3);
		}
	}

	private void showBackupPromptScreen(Screen parent, String levelName, boolean customized, Runnable callback) {
		Text text;
		Text text2;
		if (customized) {
			text = Text.translatable("selectWorld.backupQuestion.customized");
			text2 = Text.translatable("selectWorld.backupWarning.customized");
		} else {
			text = Text.translatable("selectWorld.backupQuestion.experimental");
			text2 = Text.translatable("selectWorld.backupWarning.experimental");
		}

		this.client.setScreen(new BackupPromptScreen(parent, (backup, eraseCache) -> {
			if (backup) {
				EditWorldScreen.onBackupConfirm(this.storage, levelName);
			}

			callback.run();
		}, text, text2, false));
	}

	public static void tryLoad(MinecraftClient client, CreateWorldScreen parent, Lifecycle lifecycle, Runnable loader, boolean bypassWarnings) {
		BooleanConsumer booleanConsumer = confirmed -> {
			if (confirmed) {
				loader.run();
			} else {
				client.setScreen(parent);
			}
		};
		if (bypassWarnings || lifecycle == Lifecycle.stable()) {
			loader.run();
		} else if (lifecycle == Lifecycle.experimental()) {
			client.setScreen(
				new ConfirmScreen(
					booleanConsumer, Text.translatable("selectWorld.warning.experimental.title"), Text.translatable("selectWorld.warning.experimental.question")
				)
			);
		} else {
			client.setScreen(
				new ConfirmScreen(booleanConsumer, Text.translatable("selectWorld.warning.deprecated.title"), Text.translatable("selectWorld.warning.deprecated.question"))
			);
		}
	}
}
