package net.minecraft.stats;

import com.google.common.collect.Sets;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParseException;
import com.mojang.datafixers.DataFixer;
import com.mojang.logging.LogUtils;
import com.mojang.serialization.Codec;
import com.mojang.serialization.DataResult;
import com.mojang.serialization.Dynamic;
import com.mojang.serialization.JsonOps;
import it.unimi.dsi.fastutil.objects.Object2IntMap;
import it.unimi.dsi.fastutil.objects.Object2IntOpenHashMap;
import java.io.File;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
import net.minecraft.SharedConstants;
import net.minecraft.Util;
import net.minecraft.core.registries.BuiltInRegistries;
import net.minecraft.nbt.NbtUtils;
import net.minecraft.network.protocol.game.ClientboundAwardStatsPacket;
import net.minecraft.server.MinecraftServer;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.util.StrictJsonParser;
import net.minecraft.util.datafix.DataFixTypes;
import net.minecraft.world.entity.player.Player;
import org.apache.commons.io.FileUtils;
import org.slf4j.Logger;

public class ServerStatsCounter extends StatsCounter {
    private static final Logger LOGGER = LogUtils.getLogger();
    private static final Codec<Map<Stat<?>, Integer>> STATS_CODEC = Codec.dispatchedMap(
            BuiltInRegistries.STAT_TYPE.byNameCodec(), Util.memoize(ServerStatsCounter::createTypedStatsCodec)
        )
        .xmap(map -> {
            Map<Stat<?>, Integer> map1 = new HashMap<>();
            map.forEach((statType, map2) -> map1.putAll((Map<? extends Stat<?>, ? extends Integer>)map2));
            return map1;
        }, map -> map.entrySet().stream().collect(Collectors.groupingBy(entry -> entry.getKey().getType(), Util.toMap())));
    private final MinecraftServer server;
    private final File file;
    private final Set<Stat<?>> dirty = Sets.newHashSet();

    private static <T> Codec<Map<Stat<?>, Integer>> createTypedStatsCodec(StatType<T> type) {
        Codec<T> codec = type.getRegistry().byNameCodec();
        Codec<Stat<?>> codec1 = codec.flatComapMap(
            type::get,
            stat -> stat.getType() == type
                ? DataResult.success((T)stat.getValue())
                : DataResult.error(() -> "Expected type " + type + ", but got " + stat.getType())
        );
        return Codec.unboundedMap(codec1, Codec.INT);
    }

    public ServerStatsCounter(MinecraftServer server, File file) {
        this.server = server;
        this.file = file;
        if (file.isFile()) {
            try {
                this.parseLocal(server.getFixerUpper(), FileUtils.readFileToString(file));
            } catch (IOException var4) {
                LOGGER.error("Couldn't read statistics file {}", file, var4);
            } catch (JsonParseException var5) {
                LOGGER.error("Couldn't parse statistics file {}", file, var5);
            }
        }
        // Paper start - Moved after stat fetching for player state file
        // Moves the loading after vanilla loading, so it overrides the values.
        // Disables saving any forced stats, so it stays at the same value (without enabling disableStatSaving)
        // Fixes stat initialization to not cause a NullPointerException
        // Spigot start
        for (Map.Entry<net.minecraft.resources.ResourceLocation, Integer> entry : org.spigotmc.SpigotConfig.forcedStats.entrySet()) {
            Stat<net.minecraft.resources.ResourceLocation> wrapper = Stats.CUSTOM.get(java.util.Objects.requireNonNull(BuiltInRegistries.CUSTOM_STAT.getValue(entry.getKey()))); // Paper - ensured by SpigotConfig#stats
            this.stats.put(wrapper, entry.getValue().intValue());
        }
        // Spigot end
        // Paper end - Moved after stat fetching for player state file
    }

    public void save() {
        if (org.spigotmc.SpigotConfig.disableStatSaving) return; // Spigot
        try {
            FileUtils.writeStringToFile(this.file, this.toJson());
        } catch (IOException var2) {
            LOGGER.error("Couldn't save stats", (Throwable)var2);
        }
    }

    @Override
    public void setValue(Player player, Stat<?> stat, int i) {
        if (org.spigotmc.SpigotConfig.disableStatSaving) return; // Spigot
        if (stat.getType() == Stats.CUSTOM && stat.getValue() instanceof final net.minecraft.resources.ResourceLocation resourceLocation && org.spigotmc.SpigotConfig.forcedStats.get(resourceLocation) != null) return; // Paper - disable saving forced stats
        super.setValue(player, stat, i);
        this.dirty.add(stat);
    }

    private Set<Stat<?>> getDirty() {
        Set<Stat<?>> set = Sets.newHashSet(this.dirty);
        this.dirty.clear();
        return set;
    }

    public void parseLocal(DataFixer fixerUpper, String json) {
        try {
            JsonElement jsonElement = StrictJsonParser.parse(json);
            if (jsonElement.isJsonNull()) {
                LOGGER.error("Unable to parse Stat data from {}", this.file);
                return;
            }

            Dynamic<JsonElement> dynamic = new Dynamic<>(JsonOps.INSTANCE, jsonElement);
            dynamic = DataFixTypes.STATS.updateToCurrentVersion(fixerUpper, dynamic, NbtUtils.getDataVersion(dynamic, 1343));
            this.stats
                .putAll(
                    STATS_CODEC.parse(dynamic.get("stats").orElseEmptyMap())
                        .resultOrPartial(string -> LOGGER.error("Failed to parse statistics for {}: {}", this.file, string))
                        .orElse(Map.of())
                );
        } catch (JsonParseException var5) {
            LOGGER.error("Unable to parse Stat data from {}", this.file, var5);
        }
    }

    protected String toJson() {
        JsonObject jsonObject = new JsonObject();
        jsonObject.add("stats", STATS_CODEC.encodeStart(JsonOps.INSTANCE, this.stats).getOrThrow());
        jsonObject.addProperty("DataVersion", SharedConstants.getCurrentVersion().dataVersion().version());
        return jsonObject.toString();
    }

    public void markAllDirty() {
        this.dirty.addAll(this.stats.keySet());
    }

    public void sendStats(ServerPlayer player) {
        Object2IntMap<Stat<?>> map = new Object2IntOpenHashMap<>();

        for (Stat<?> stat : this.getDirty()) {
            map.put(stat, this.getValue(stat));
        }

        player.connection.send(new ClientboundAwardStatsPacket(map));
    }
}
