package net.minecraft.server.dedicated;

import com.google.common.base.Splitter;
import com.google.common.base.Strings;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.mojang.logging.LogUtils;
import com.mojang.serialization.Dynamic;
import com.mojang.serialization.JsonOps;
import java.nio.charset.StandardCharsets;
import java.nio.file.Path;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Optional;
import java.util.Properties;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
import java.util.regex.Pattern;
import javax.annotation.Nullable;
import net.minecraft.core.Holder;
import net.minecraft.core.HolderLookup;
import net.minecraft.core.RegistryAccess;
import net.minecraft.core.registries.Registries;
import net.minecraft.network.chat.Component;
import net.minecraft.network.chat.ComponentSerialization;
import net.minecraft.resources.RegistryOps;
import net.minecraft.resources.ResourceKey;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.server.MinecraftServer;
import net.minecraft.util.GsonHelper;
import net.minecraft.util.Mth;
import net.minecraft.util.StrictJsonParser;
import net.minecraft.world.Difficulty;
import net.minecraft.world.level.DataPackConfig;
import net.minecraft.world.level.GameType;
import net.minecraft.world.level.WorldDataConfiguration;
import net.minecraft.world.level.levelgen.FlatLevelSource;
import net.minecraft.world.level.levelgen.WorldDimensions;
import net.minecraft.world.level.levelgen.WorldOptions;
import net.minecraft.world.level.levelgen.flat.FlatLevelGeneratorSettings;
import net.minecraft.world.level.levelgen.presets.WorldPreset;
import net.minecraft.world.level.levelgen.presets.WorldPresets;
import org.slf4j.Logger;

public class DedicatedServerProperties extends Settings<DedicatedServerProperties> {
    static final Logger LOGGER = LogUtils.getLogger();
    private static final Pattern SHA1 = Pattern.compile("^[a-fA-F0-9]{40}$");
    private static final Splitter COMMA_SPLITTER = Splitter.on(',').trimResults();
    public final boolean debug = this.get("debug", false); // CraftBukkit
    public final boolean onlineMode = this.get("online-mode", true);
    public final boolean preventProxyConnections = this.get("prevent-proxy-connections", false);
    public final String serverIp = this.get("server-ip", "");
    public final boolean pvp = this.get("pvp", true);
    public final boolean allowFlight = this.get("allow-flight", false);
    public final String motd = this.get("motd", "A Minecraft Server");
    public final String bugReportLink = this.get("bug-report-link", "");
    public final boolean forceGameMode = this.get("force-gamemode", false);
    public final boolean enforceWhitelist = this.get("enforce-whitelist", false);
    public final Difficulty difficulty = this.get(
        "difficulty", dispatchNumberOrString(Difficulty::byId, Difficulty::byName), Difficulty::getKey, Difficulty.EASY
    );
    public final GameType gamemode = this.get("gamemode", dispatchNumberOrString(GameType::byId, GameType::byName), GameType::getName, GameType.SURVIVAL);
    public final String levelName = this.get("level-name", "world");
    public final int serverPort = this.get("server-port", 25565);
    @Nullable
    public final Boolean announcePlayerAchievements = this.getLegacyBoolean("announce-player-achievements");
    public final boolean enableQuery = this.get("enable-query", false);
    public final int queryPort = this.get("query.port", 25565);
    public final boolean enableRcon = this.get("enable-rcon", false);
    public final int rconPort = this.get("rcon.port", 25575);
    public final String rconPassword = this.get("rcon.password", "");
    public final boolean hardcore = this.get("hardcore", false);
    public final boolean allowNether = this.get("allow-nether", true);
    public final boolean spawnMonsters = this.get("spawn-monsters", true);
    public final boolean useNativeTransport = this.get("use-native-transport", true);
    public final boolean enableCommandBlock = this.get("enable-command-block", false);
    public final int spawnProtection = this.get("spawn-protection", 16);
    public final int opPermissionLevel = this.get("op-permission-level", 4);
    public final int functionPermissionLevel = this.get("function-permission-level", 2);
    public final long maxTickTime = this.get("max-tick-time", TimeUnit.MINUTES.toMillis(1L));
    public final int maxChainedNeighborUpdates = this.get("max-chained-neighbor-updates", 1000000);
    public final int rateLimitPacketsPerSecond = this.get("rate-limit", 0);
    public final int viewDistance = this.get("view-distance", 10);
    public final int simulationDistance = this.get("simulation-distance", 10);
    public final int maxPlayers = this.get("max-players", 20);
    public final int networkCompressionThreshold = this.get("network-compression-threshold", 256);
    public final boolean broadcastRconToOps = this.get("broadcast-rcon-to-ops", true);
    public final boolean broadcastConsoleToOps = this.get("broadcast-console-to-ops", true);
    public final int maxWorldSize = this.get("max-world-size", property -> Mth.clamp(property, 1, 29999984), 29999984);
    public final boolean syncChunkWrites = this.get("sync-chunk-writes", true) && Boolean.getBoolean("Paper.enable-sync-chunk-writes"); // Paper - Hide sync chunk writes behind flag
    public final String regionFileComression = this.get("region-file-compression", "deflate");
    public final boolean enableJmxMonitoring = this.get("enable-jmx-monitoring", false);
    public final boolean enableStatus = this.get("enable-status", true);
    public final boolean hideOnlinePlayers = this.get("hide-online-players", false);
    public final int entityBroadcastRangePercentage = this.get("entity-broadcast-range-percentage", property -> Mth.clamp(property, 10, 1000), 100);
    public final String textFilteringConfig = this.get("text-filtering-config", "");
    public final int textFilteringVersion = this.get("text-filtering-version", 0);
    public final Optional<MinecraftServer.ServerResourcePackInfo> serverResourcePackInfo;
    public final DataPackConfig initialDataPackConfiguration;
    public final Settings<DedicatedServerProperties>.MutableValue<Integer> playerIdleTimeout = this.getMutable("player-idle-timeout", 0);
    public final Settings<DedicatedServerProperties>.MutableValue<Boolean> whiteList = this.getMutable("white-list", false);
    public final boolean enforceSecureProfile = this.get("enforce-secure-profile", true);
    public final boolean logIPs = this.get("log-ips", true);
    public int pauseWhenEmptySeconds = this.get("pause-when-empty-seconds", -1); // Paper - disable tick sleeping by default 
    private final DedicatedServerProperties.WorldDimensionData worldDimensionData;
    public final WorldOptions worldOptions;
    public boolean acceptsTransfers = this.get("accepts-transfers", false);
    public final String rconIp; // Paper - Configurable rcon ip

    // CraftBukkit start
    public DedicatedServerProperties(Properties properties, joptsimple.OptionSet optionset) {
        super(properties, optionset);
        // CraftBukkit end
        String string = this.get("level-seed", "");
        boolean flag = this.get("generate-structures", true);
        long l = WorldOptions.parseSeed(string).orElse(WorldOptions.randomSeed());
        // Leaf start - Matter - Secure Seed
        if (me.earthme.luminol.config.modules.function.SecureSeedConfig.enabled) {
            String featureSeedStr = this.get("feature-level-seed", "");
            long[] featureSeed = su.plo.matter.Globals.parseSeed(featureSeedStr)
                .orElse(su.plo.matter.Globals.createRandomWorldSeed());

            this.worldOptions = new WorldOptions(l, featureSeed, flag, false);
        } else {
            this.worldOptions = new WorldOptions(l, flag, false);
        }
        // Leaf end - Matter - Secure Seed
        this.worldDimensionData = new DedicatedServerProperties.WorldDimensionData(
            this.get("generator-settings", property -> GsonHelper.parse(!property.isEmpty() ? property : "{}"), new JsonObject()),
            this.get("level-type", property -> property.toLowerCase(Locale.ROOT), WorldPresets.NORMAL.location().toString())
        );
        this.serverResourcePackInfo = getServerPackInfo(
            this.get("resource-pack-id", ""),
            this.get("resource-pack", ""),
            this.get("resource-pack-sha1", ""),
            this.getLegacyString("resource-pack-hash"),
            this.get("require-resource-pack", false),
            this.get("resource-pack-prompt", "")
        );
        this.initialDataPackConfiguration = getDatapackConfig(
            this.get("initial-enabled-packs", String.join(",", WorldDataConfiguration.DEFAULT.dataPacks().getEnabled())),
            this.get("initial-disabled-packs", String.join(",", WorldDataConfiguration.DEFAULT.dataPacks().getDisabled()))
        );
        // Paper start - Configurable rcon ip
        final String rconIp = this.getStringRaw("rcon.ip");
        this.rconIp = rconIp == null ? this.serverIp : rconIp;
        // Paper end - Configurable rcon ip
    }

    // CraftBukkit start
    public static DedicatedServerProperties fromFile(Path path, joptsimple.OptionSet optionset) {
        return new DedicatedServerProperties(loadFromFile(path), optionset);
    }

    @Override
    public DedicatedServerProperties reload(RegistryAccess registryAccess, Properties properties, joptsimple.OptionSet options) {
        return new DedicatedServerProperties(properties, options);
        // CraftBukkit end
    }

    @Nullable
    private static Component parseResourcePackPrompt(String json) {
        if (!Strings.isNullOrEmpty(json)) {
            try {
                JsonElement jsonElement = StrictJsonParser.parse(json);
                return ComponentSerialization.CODEC
                    .parse(RegistryAccess.EMPTY.createSerializationContext(JsonOps.INSTANCE), jsonElement)
                    .resultOrPartial(string -> LOGGER.warn("Failed to parse resource pack prompt '{}': {}", json, string))
                    .orElse(null);
            } catch (Exception var2) {
                LOGGER.warn("Failed to parse resource pack prompt '{}'", json, var2);
            }
        }

        return null;
    }

    private static Optional<MinecraftServer.ServerResourcePackInfo> getServerPackInfo(
        String id, String url, String sha1, @Nullable String hash, boolean isRequired, String promptJson
    ) {
        if (url.isEmpty()) {
            return Optional.empty();
        } else {
            String string;
            if (!sha1.isEmpty()) {
                string = sha1;
                if (!Strings.isNullOrEmpty(hash)) {
                    LOGGER.warn("resource-pack-hash is deprecated and found along side resource-pack-sha1. resource-pack-hash will be ignored.");
                }
            } else if (!Strings.isNullOrEmpty(hash)) {
                LOGGER.warn("resource-pack-hash is deprecated. Please use resource-pack-sha1 instead.");
                string = hash;
            } else {
                string = "";
            }

            if (string.isEmpty()) {
                LOGGER.warn(
                    "You specified a resource pack without providing a sha1 hash. Pack will be updated on the client only if you change the name of the pack."
                );
            } else if (!SHA1.matcher(string).matches()) {
                LOGGER.warn("Invalid sha1 for resource-pack-sha1");
            }

            Component component = parseResourcePackPrompt(promptJson);
            UUID uuid;
            if (id.isEmpty()) {
                uuid = UUID.nameUUIDFromBytes(url.getBytes(StandardCharsets.UTF_8));
                LOGGER.warn("resource-pack-id missing, using default of {}", uuid);
            } else {
                try {
                    uuid = UUID.fromString(id);
                } catch (IllegalArgumentException var10) {
                    LOGGER.warn("Failed to parse '{}' into UUID", id);
                    return Optional.empty();
                }
            }

            return Optional.of(new MinecraftServer.ServerResourcePackInfo(uuid, url, string, isRequired, component));
        }
    }

    private static DataPackConfig getDatapackConfig(String initalEnabledPacks, String initialDisabledPacks) {
        List<String> parts = COMMA_SPLITTER.splitToList(initalEnabledPacks);
        List<String> parts1 = COMMA_SPLITTER.splitToList(initialDisabledPacks);
        return new DataPackConfig(parts, parts1);
    }

    public WorldDimensions createDimensions(HolderLookup.Provider registries) {
        return this.worldDimensionData.create(registries);
    }

    public record WorldDimensionData(JsonObject generatorSettings, String levelType) {
        private static final Map<String, ResourceKey<WorldPreset>> LEGACY_PRESET_NAMES = Map.of(
            "default", WorldPresets.NORMAL, "largebiomes", WorldPresets.LARGE_BIOMES
        );

        public WorldDimensions create(HolderLookup.Provider registries) {
            HolderLookup<WorldPreset> holderLookup = registries.lookupOrThrow(Registries.WORLD_PRESET);
            Holder.Reference<WorldPreset> reference = holderLookup.get(WorldPresets.NORMAL)
                .or(() -> holderLookup.listElements().findAny())
                .orElseThrow(() -> new IllegalStateException("Invalid datapack contents: can't find default preset"));
            Holder<WorldPreset> holder = Optional.ofNullable(ResourceLocation.tryParse(this.levelType))
                .map(presetPath -> ResourceKey.create(Registries.WORLD_PRESET, presetPath))
                .or(() -> Optional.ofNullable(LEGACY_PRESET_NAMES.get(this.levelType)))
                .flatMap(holderLookup::get)
                .orElseGet(() -> {
                    DedicatedServerProperties.LOGGER.warn("Failed to parse level-type {}, defaulting to {}", this.levelType, reference.key().location());
                    return reference;
                });
            WorldDimensions worldDimensions = holder.value().createWorldDimensions();
            if (holder.is(WorldPresets.FLAT)) {
                RegistryOps<JsonElement> registryOps = registries.createSerializationContext(JsonOps.INSTANCE);
                Optional<FlatLevelGeneratorSettings> optional = FlatLevelGeneratorSettings.CODEC
                    .parse(new Dynamic<>(registryOps, this.generatorSettings()))
                    .resultOrPartial(DedicatedServerProperties.LOGGER::error);
                if (optional.isPresent()) {
                    return worldDimensions.replaceOverworldGenerator(registries, new FlatLevelSource(optional.get()));
                }
            }

            return worldDimensions;
        }
    }
}
