// 
// Decompiled by Procyon v0.6.0
// 

package com.hypixel.hytale.server.core.command.commands.utility;

import com.hypixel.hytale.server.core.command.system.arguments.system.Argument;
import java.util.concurrent.CompletionStage;
import org.bson.BsonDocument;
import com.hypixel.hytale.server.core.prefab.selection.standard.BlockSelection;
import java.util.concurrent.Executor;
import com.hypixel.hytale.server.core.prefab.config.SelectionPrefabSerializer;
import com.hypixel.hytale.server.core.util.BsonUtil;
import java.util.concurrent.TimeUnit;
import javax.annotation.Nullable;
import java.util.stream.Stream;
import com.hypixel.hytale.server.core.util.io.FileUtil;
import java.io.IOException;
import com.hypixel.hytale.sneakythrow.SneakyThrow;
import java.nio.file.attribute.FileAttribute;
import java.util.UUID;
import com.hypixel.hytale.server.core.universe.world.storage.resources.IResourceStorageProvider;
import com.hypixel.hytale.server.core.universe.world.storage.resources.EmptyResourceStorageProvider;
import com.hypixel.hytale.server.core.universe.world.storage.provider.IChunkStorageProvider;
import com.hypixel.hytale.server.core.universe.world.storage.provider.EmptyChunkStorageProvider;
import com.hypixel.hytale.server.core.universe.world.worldgen.provider.IWorldGenProvider;
import com.hypixel.hytale.server.core.universe.world.worldgen.provider.DummyWorldGenProvider;
import com.hypixel.hytale.server.core.universe.world.WorldConfig;
import java.nio.file.Files;
import java.nio.file.LinkOption;
import com.hypixel.hytale.server.core.util.message.MessageFormat;
import java.util.Set;
import java.util.stream.Collector;
import java.util.stream.Collectors;
import java.util.function.Function;
import java.util.Collection;
import com.hypixel.hytale.server.core.universe.world.World;
import java.nio.file.Path;
import com.hypixel.hytale.server.core.prefab.PrefabStore;
import java.util.List;
import java.nio.file.Paths;
import it.unimi.dsi.fastutil.objects.ObjectArrayList;
import com.hypixel.hytale.math.util.ChunkUtil;
import com.hypixel.hytale.server.core.universe.Universe;
import java.util.concurrent.CompletableFuture;
import com.hypixel.hytale.server.core.command.system.CommandContext;
import com.hypixel.hytale.server.core.command.system.arguments.types.ArgumentType;
import com.hypixel.hytale.server.core.command.system.arguments.types.ArgTypes;
import com.hypixel.hytale.server.core.command.system.arguments.system.DefaultArg;
import com.hypixel.hytale.server.core.command.system.arguments.system.OptionalArg;
import com.hypixel.hytale.server.core.command.system.arguments.system.FlagArg;
import javax.annotation.Nonnull;
import com.hypixel.hytale.server.core.Message;
import com.hypixel.hytale.server.core.command.system.basecommands.AbstractAsyncCommand;

public class ConvertPrefabsCommand extends AbstractAsyncCommand
{
    private static final String UNABLE_TO_LOAD_MODEL = "Unable to load entity with model ";
    private static final String FAILED_TO_FIND_BLOCK = "Failed to find block ";
    private static final int BATCH_SIZE = 10;
    private static final long DELAY_BETWEEN_BATCHES_MS = 50L;
    @Nonnull
    private static final Message MESSAGE_COMMANDS_CONVERT_PREFABS_FAILED;
    @Nonnull
    private static final Message MESSAGE_COMMANDS_CONVERT_PREFABS_DEFAULT_WORLD_NULL;
    @Nonnull
    private final FlagArg blocksFlag;
    @Nonnull
    private final FlagArg fillerFlag;
    @Nonnull
    private final FlagArg relativeFlag;
    @Nonnull
    private final FlagArg entitiesFlag;
    private final FlagArg destructiveFlag;
    @Nonnull
    private final OptionalArg<String> pathArg;
    @Nonnull
    private final DefaultArg<String> storeArg;
    
    public ConvertPrefabsCommand() {
        super("convertprefabs", "server.commands.convertprefabs.desc");
        this.blocksFlag = this.withFlagArg("blocks", "server.commands.convertprefabs.blocks.desc");
        this.fillerFlag = this.withFlagArg("filler", "server.commands.convertprefabs.filler.desc");
        this.relativeFlag = this.withFlagArg("relative", "server.commands.convertprefabs.relative.desc");
        this.entitiesFlag = this.withFlagArg("entities", "server.commands.convertprefabs.entities.desc");
        this.destructiveFlag = this.withFlagArg("destructive", "server.commands.convertprefabs.destructive.desc");
        this.pathArg = this.withOptionalArg("path", "server.commands.convertprefabs.path.desc", ArgTypes.STRING);
        this.storeArg = this.withDefaultArg("store", "server.commands.convertprefabs.store.desc", ArgTypes.STRING, "asset", "server.commands.convertprefabs.store.defaultDesc");
    }
    
    @Nonnull
    @Override
    protected CompletableFuture<Void> executeAsync(@Nonnull final CommandContext context) {
        final boolean blocks = ((Argument<Arg, Boolean>)this.blocksFlag).get(context);
        final boolean filler = ((Argument<Arg, Boolean>)this.fillerFlag).get(context);
        final boolean relative = ((Argument<Arg, Boolean>)this.relativeFlag).get(context);
        final boolean entities = ((Argument<Arg, Boolean>)this.entitiesFlag).get(context);
        final boolean destructive = ((Argument<Arg, Boolean>)this.destructiveFlag).get(context);
        final World defaultWorld = Universe.get().getDefaultWorld();
        if (defaultWorld == null) {
            context.sendMessage(ConvertPrefabsCommand.MESSAGE_COMMANDS_CONVERT_PREFABS_DEFAULT_WORLD_NULL);
            return CompletableFuture.completedFuture((Void)null);
        }
        defaultWorld.getChunk(ChunkUtil.indexChunk(0, 0));
        final List<String> failed = new ObjectArrayList<String>();
        final List<String> skipped = new ObjectArrayList<String>();
        final String storeOption = this.storeArg.get(context);
        if (this.pathArg.provided(context)) {
            final Path assetPath = Paths.get(this.pathArg.get(context), new String[0]);
            return this.convertPath(assetPath, blocks, filler, relative, entities, destructive, failed, skipped).thenApply(_v -> {
                this.sendCompletionMessages(context, assetPath, failed, skipped);
                return null;
            });
        }
        final String s = storeOption;
        return switch (s) {
            case "server" -> {
                final Path assetPath = PrefabStore.get().getServerPrefabsPath();
                yield this.convertPath(assetPath, blocks, filler, relative, entities, destructive, failed, skipped).thenApply(_v -> {
                    this.sendCompletionMessages(context, assetPath, failed, skipped);
                    return null;
                });
            }
            case "asset" -> {
                final Path assetPath = PrefabStore.get().getAssetPrefabsPath();
                yield this.convertPath(assetPath, blocks, filler, relative, entities, destructive, failed, skipped).thenApply(_v -> {
                    this.sendCompletionMessages(context, assetPath, failed, skipped);
                    return null;
                });
            }
            case "worldgen" -> {
                final Path assetPath = PrefabStore.get().getWorldGenPrefabsPath();
                yield this.convertPath(assetPath, blocks, filler, relative, entities, destructive, failed, skipped).thenApply(_v -> {
                    this.sendCompletionMessages(context, assetPath, failed, skipped);
                    return null;
                });
            }
            case "all" -> {
                final Path assetPath = Path.of("", new String[0]);
                yield this.convertPath(PrefabStore.get().getWorldGenPrefabsPath(), blocks, filler, relative, entities, destructive, failed, skipped).thenCompose(_v -> this.convertPath(PrefabStore.get().getServerPrefabsPath(), blocks, filler, relative, entities, destructive, failed, skipped)).thenCompose(_v -> this.convertPath(PrefabStore.get().getAssetPrefabsPath(), blocks, filler, relative, entities, destructive, failed, skipped)).thenApply(_v -> {
                    this.sendCompletionMessages(context, assetPath, failed, skipped);
                    return null;
                });
            }
            default -> {
                context.sendMessage(Message.translation("server.commands.convertprefabs.invalidStore").param("store", storeOption));
                yield CompletableFuture.completedFuture((Void)null);
            }
        };
    }
    
    private void sendCompletionMessages(@Nonnull final CommandContext context, @Nonnull final Path assetPath, @Nonnull final List<String> failed, @Nonnull final List<String> skipped) {
        if (!skipped.isEmpty()) {
            final Message header = Message.translation("server.commands.convertprefabs.skipped");
            context.sendMessage(MessageFormat.list(header, (Collection<Message>)skipped.stream().map((Function<? super Object, ?>)Message::raw).collect((Collector<? super Object, ?, Set<? super Object>>)Collectors.toSet())));
        }
        if (!failed.isEmpty()) {
            context.sendMessage(MessageFormat.list(ConvertPrefabsCommand.MESSAGE_COMMANDS_CONVERT_PREFABS_FAILED, (Collection<Message>)failed.stream().map((Function<? super Object, ?>)Message::raw).collect((Collector<? super Object, ?, Set<? super Object>>)Collectors.toSet())));
        }
        context.sendMessage(Message.translation("server.commands.prefabConvertionDone").param("path", assetPath.toString()));
    }
    
    @Nonnull
    private CompletableFuture<Void> convertPath(@Nonnull final Path assetPath, final boolean blocks, final boolean filler, final boolean relative, final boolean entities, final boolean destructive, @Nonnull final List<String> failed, @Nonnull final List<String> skipped) {
        if (!Files.exists(assetPath, new LinkOption[0])) {
            return CompletableFuture.completedFuture((Void)null);
        }
        CompletableFuture<World> conversionWorldFuture;
        if (entities || blocks) {
            final Universe universe = Universe.get();
            final WorldConfig config = new WorldConfig();
            config.setWorldGenProvider(new DummyWorldGenProvider());
            config.setChunkStorageProvider(EmptyChunkStorageProvider.INSTANCE);
            config.setResourceStorageProvider(EmptyResourceStorageProvider.INSTANCE);
            try {
                conversionWorldFuture = universe.makeWorld("ConvertPrefabs-" + String.valueOf(UUID.randomUUID()), Files.createTempDirectory("convertprefab", (FileAttribute<?>[])new FileAttribute[0]), config);
            }
            catch (final IOException e) {
                throw SneakyThrow.sneakyThrow(e);
            }
        }
        else {
            conversionWorldFuture = null;
        }
        try (final Stream<Path> stream = Files.walk(assetPath, FileUtil.DEFAULT_WALK_TREE_OPTIONS_ARRAY)) {
            final List<Path> prefabPaths = stream.filter(path -> Files.isRegularFile(path, new LinkOption[0]) && path.toString().endsWith(".prefab.json")).collect((Collector<? super Path, ?, List<Path>>)Collectors.toList());
            if (prefabPaths.isEmpty()) {
                if (conversionWorldFuture != null) {
                    conversionWorldFuture.thenAccept(world -> Universe.get().removeWorld(world.getName()));
                }
                final CompletableFuture<Object> completedFuture = CompletableFuture.completedFuture((Object)null);
                if (stream != null) {
                    stream.close();
                }
                return (CompletableFuture<Void>)completedFuture;
            }
            final CompletableFuture<Object> thenApply = this.processPrefabsInBatches(prefabPaths, blocks, filler, relative, entities, destructive, conversionWorldFuture, failed, skipped).thenApply(_v -> {
                if (conversionWorldFuture != null) {
                    conversionWorldFuture.thenAccept(world -> Universe.get().removeWorld(world.getName()));
                }
                return null;
            });
            return (CompletableFuture<Void>)thenApply;
        }
        catch (final IOException e2) {
            throw SneakyThrow.sneakyThrow(e2);
        }
    }
    
    @Nonnull
    private CompletableFuture<Void> processPrefabsInBatches(@Nonnull final List<Path> prefabPaths, final boolean blocks, final boolean filler, final boolean relative, final boolean entities, final boolean destructive, @Nullable final CompletableFuture<World> conversionWorldFuture, @Nonnull final List<String> failed, @Nonnull final List<String> skipped) {
        CompletableFuture<Void> result = CompletableFuture.completedFuture((Void)null);
        for (int i = 0; i < prefabPaths.size(); i += 10) {
            final int batchEnd = Math.min(i + 10, prefabPaths.size());
            final List<Path> batch = prefabPaths.subList(i, batchEnd);
            final int batchIndex = i / 10;
            if (batchIndex > 0) {
                result = result.thenCompose(_v -> CompletableFuture.runAsync(() -> {}, CompletableFuture.delayedExecutor(50L, TimeUnit.MILLISECONDS)));
            }
            final CompletableFuture<?>[] batchFutures = batch.stream().map(path -> this.processPrefab(path, blocks, filler, relative, entities, destructive, conversionWorldFuture, failed, skipped)).toArray(CompletableFuture[]::new);
            result = result.thenCompose(_v -> CompletableFuture.allOf((CompletableFuture<?>[])batchFutures));
        }
        return result;
    }
    
    @Nonnull
    private CompletableFuture<Void> processPrefab(@Nonnull final Path path, final boolean blocks, final boolean filler, final boolean relative, final boolean entities, final boolean destructive, @Nullable final CompletableFuture<World> conversionWorldFuture, @Nonnull final List<String> failed, @Nonnull final List<String> skipped) {
        return BsonUtil.readDocument(path, false).thenApply(document -> {
            BlockSelection prefab = SelectionPrefabSerializer.deserialize(document);
            if (filler) {
                prefab.tryFixFiller(destructive);
            }
            if (relative) {
                prefab = prefab.relativize();
            }
            return prefab;
        }).thenCompose(prefab -> {
            if (entities && conversionWorldFuture != null) {
                return conversionWorldFuture.thenCompose(world -> CompletableFuture.runAsync(() -> {
                    try {
                        prefab.reserializeEntities(world.getEntityStore().getStore(), destructive);
                    }
                    catch (final IOException e) {
                        throw SneakyThrow.sneakyThrow(e);
                    }
                }, world)).thenApply(_v -> prefab);
            }
            else {
                return CompletableFuture.completedFuture(prefab);
            }
        }).thenCompose(prefab -> {
            if (blocks && conversionWorldFuture != null) {
                return conversionWorldFuture.thenCompose(world -> CompletableFuture.runAsync(() -> prefab.reserializeBlockStates(world.getChunkStore(), destructive), world)).thenApply(_v -> prefab);
            }
            else {
                return CompletableFuture.completedFuture(prefab);
            }
        }).thenCompose(prefab -> {
            final BsonDocument newDocument = SelectionPrefabSerializer.serialize(prefab);
            return BsonUtil.writeDocument(path, newDocument, false);
        }).exceptionally(throwable -> {
            final String message = (throwable.getCause() != null) ? throwable.getCause().getMessage() : null;
            if (message != null) {
                if (message.contains("Failed to find block ")) {
                    if (message.substring("Failed to find block ".length()).contains("%")) {
                        skipped.add("Skipped prefab " + String.valueOf(path) + " because it contains block % chance.");
                        return null;
                    }
                    else {
                        failed.add("Failed to update " + String.valueOf(path) + " because " + message);
                        return null;
                    }
                }
                else if (message.contains("Unable to load entity with model ")) {
                    failed.add("Failed to update " + String.valueOf(path) + " because " + message);
                    return null;
                }
            }
            failed.add("Failed to update " + String.valueOf(path) + " because " + String.valueOf((message != null) ? message : ((throwable.getCause() != null) ? throwable.getCause().getClass() : throwable.getClass())));
            if (throwable.getCause() != null) {
                new Exception("Failed to update " + String.valueOf(path), throwable.getCause()).printStackTrace();
            }
            return null;
        });
    }
    
    static {
        MESSAGE_COMMANDS_CONVERT_PREFABS_FAILED = Message.translation("server.commands.convertprefabs.failed");
        MESSAGE_COMMANDS_CONVERT_PREFABS_DEFAULT_WORLD_NULL = Message.translation("server.commands.convertprefabs.defaultWorldNull");
    }
}
