/*
 *  Copyright 2025-2026 R3944Realms
 *
 *    Licensed under the Apache License, Version 2.0 (the "License");
 *    you may not use this file except in compliance with the License.
 *    You may obtain a copy of the License at
 *
 *        http://www.apache.org/licenses/LICENSE-2.0
 *
 *    Unless required by applicable law or agreed to in writing, software
 *    distributed under the License is distributed on an "AS IS" BASIS,
 *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *    See the License for the specific language governing permissions and
 *    limitations under the License.
 */

package top.r3944realms.eroticdungeongame.core.network.toClient;

import com.mojang.authlib.GameProfile;
import net.minecraft.Optionull;
import net.minecraft.client.Minecraft;
import net.minecraft.client.multiplayer.ClientPacketListener;
import net.minecraft.client.multiplayer.PlayerInfo;
import net.minecraft.network.FriendlyByteBuf;
import net.minecraft.network.chat.Component;
import net.minecraft.network.chat.RemoteChatSession;
import net.minecraft.network.protocol.game.ClientboundPlayerInfoUpdatePacket;
import net.minecraft.world.level.GameType;
import net.minecraftforge.api.distmarker.Dist;
import net.minecraftforge.api.distmarker.OnlyIn;
import net.minecraftforge.network.NetworkEvent;
import org.jetbrains.annotations.Contract;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import top.r3944realms.eroticdungeongame.EroticDungeon;
import top.r3944realms.eroticdungeongame.client.ClientNPCManager;
import top.r3944realms.eroticdungeongame.content.entity.npc.NPCServerPlayer;
import top.r3944realms.eroticdungeongame.util.IEDGClientPacketListener;

import java.util.*;
import java.util.function.Supplier;

public final class NPCInfoUpdatePacket {
    private final EnumSet<Action> actions;
    private final List<Entry> entries;

    public List<Entry> newEntries() {
        return this.actions.contains(Action.ADD_NPC) ? this.entries : List.of();
    }

    public NPCInfoUpdatePacket(EnumSet<Action> actions, List<Entry> npcs) {
        this.actions = actions;
        this.entries = npcs;
    }

    public NPCInfoUpdatePacket(Action action, NPCServerPlayer npc) {
        this(EnumSet.of(action), List.of(new Entry(npc)));
    }

    public NPCInfoUpdatePacket(EnumSet<Action> actions, @NotNull Collection<NPCServerPlayer> players) {
        this(actions, players.stream().map(Entry::new).toList());
    }

    public NPCInfoUpdatePacket(@NotNull FriendlyByteBuf buf) {
        this.actions = buf.readEnumSet(Action.class);
        this.entries = buf.readList((read) -> {
            EntryBuilder builder = new EntryBuilder(read.readUUID());

            for(Action action : this.actions) {
                action.reader.read(builder, read);
            }

            return builder.build();
        });
    }
    public void write(@NotNull FriendlyByteBuf buffer) {
        buffer.writeEnumSet(this.actions, Action.class);
        buffer.writeCollection(this.entries, (buf, entry) -> {
            buf.writeUUID(entry.uuid());

            for(Action action : this.actions) {
                action.writer.write(buf, entry);
            }
        });
    }
    public static @NotNull NPCInfoUpdatePacket createNPCPlayerInitializing(Collection<NPCServerPlayer> players) {
        EnumSet<Action> enumset = EnumSet.of(Action.ADD_NPC, Action.INITIALIZE_CHAT, Action.UPDATE_GAME_MODE, Action.UPDATE_LISTED, Action.UPDATE_LATENCY, Action.UPDATE_DISPLAY_NAME);
        return new NPCInfoUpdatePacket(enumset, players);
    }

    public static void handle(NPCInfoUpdatePacket packet, @NotNull Supplier<NetworkEvent.Context> contextSupplier) {
        NetworkEvent.Context context = contextSupplier.get();
        context.enqueueWork(() -> {
            Minecraft minecraft = Minecraft.getInstance();
            ClientPacketListener clientPacketListener = minecraft.getConnection();
            if (clientPacketListener instanceof IEDGClientPacketListener iedgClientPacketListener) {
                for (Entry entry : packet.newEntries()) {
                    PlayerInfo npcInfo = new PlayerInfo(entry.profile, false);
                    iedgClientPacketListener.getNPCPlayerInfoMap().putIfAbsent(entry.uuid(), npcInfo);
                    ClientNPCManager.processPendingNPCs(npcInfo);
                }
                for (Entry entry : packet.entries) {
                    PlayerInfo npcInfo = iedgClientPacketListener.getNPCPlayerInfoMap().get(entry.uuid);
                    if (npcInfo == null) {
                        EroticDungeon.getLogger().warn("Ignoring npc player info update for unknown npc player {}", entry.uuid());
                    } else {
                        for(Action action : packet.actions()) {
                            applyPlayerInfoUpdate(action, entry, npcInfo);
                        }
                    }
                }
            }
        });
        context.setPacketHandled(true);
    }
    @OnlyIn(Dist.CLIENT)
    private static void applyPlayerInfoUpdate(@NotNull Action action, Entry entry, PlayerInfo playerInfo) {
        Minecraft minecraft = Minecraft.getInstance();
        ClientPacketListener clientPacketListener = minecraft.getConnection();
        if (clientPacketListener instanceof IEDGClientPacketListener iedgClientPacketListener) {
            switch (action) {
                case INITIALIZE_CHAT:
                    ClientboundPlayerInfoUpdatePacket.Entry copyEntry = new ClientboundPlayerInfoUpdatePacket.Entry(
                            entry.uuid, entry.profile, entry.listed, entry.latency, entry.gameMode, entry.displayName, entry.chatSession
                    );
                    clientPacketListener.initializeChatSession(copyEntry, playerInfo);
                    break;
                case UPDATE_GAME_MODE:

                    if (playerInfo.getGameMode() != entry.gameMode() && minecraft.player != null && minecraft.player.getUUID().equals(entry.uuid())) {
                        minecraft.player.onGameModeChanged(entry.gameMode());
                    }

                    playerInfo.setGameMode(entry.gameMode());
                    break;
                case UPDATE_LISTED:
                    if (entry.listed()) {
                        iedgClientPacketListener.getListedNPCPlayers().add(playerInfo);
                    } else {
                        iedgClientPacketListener.getListedNPCPlayers().remove(playerInfo);
                    }
                    break;
                case UPDATE_LATENCY:
                    playerInfo.setLatency(entry.latency());
                    break;
                case UPDATE_DISPLAY_NAME:
                    playerInfo.setTabListDisplayName(entry.displayName());
            }
        }

    }
    public EnumSet<Action> actions() {
        return actions;
    }

    public List<Entry> npcs() {
        return entries;
    }

    @Override
    public boolean equals(Object obj) {
        if (obj == this) return true;
        if (obj == null || obj.getClass() != this.getClass()) return false;
        var that = (NPCInfoUpdatePacket) obj;
        return Objects.equals(this.actions, that.actions) &&
                Objects.equals(this.entries, that.entries);
    }

    @Override
    public int hashCode() {
        return Objects.hash(actions, entries);
    }

    @Contract(pure = true)
    @Override
    public @NotNull String toString() {
        return "NPCInfoUpdatePacket[" +
                "actions=" + actions + ", " +
                "npcs=" + entries + ']';
    }


    public enum Action {
        ADD_NPC((entryBuilder, buf) -> {
            GameProfile gameprofile = new GameProfile(entryBuilder.profileId, buf.readUtf(16));
            gameprofile.getProperties().putAll(buf.readGameProfileProperties());
            entryBuilder.profile = gameprofile;
        }, (buf, entry) -> {
            buf.writeUtf(entry.profile().getName(), 16);
            buf.writeGameProfileProperties(entry.profile().getProperties());
        }),
        INITIALIZE_CHAT((entryBuilder, buf) -> entryBuilder.chatSession = buf.readNullable(RemoteChatSession.Data::read), (buf, entry) -> buf.writeNullable(entry.chatSession, RemoteChatSession.Data::write)),
        UPDATE_GAME_MODE((entryBuilder, buf) -> entryBuilder.gameMode = GameType.byId(buf.readVarInt()), (buf, entry) -> buf.writeVarInt(entry.gameMode().getId())),
        UPDATE_LISTED((entryBuilder, buf) -> entryBuilder.listed = buf.readBoolean(), (buf, entry) -> buf.writeBoolean(entry.listed())),
        UPDATE_LATENCY((entryBuilder, buf) -> entryBuilder.latency = buf.readVarInt(), (buf, entry) -> buf.writeVarInt(entry.latency())),
        UPDATE_DISPLAY_NAME((entryBuilder, buf) -> entryBuilder.displayName = buf.readNullable(FriendlyByteBuf::readComponent), (buf, entry) -> buf.writeNullable(entry.displayName(), FriendlyByteBuf::writeComponent));
        final Reader reader;
        final Writer writer;

        Action(Reader reader, Writer writer) {
            this.reader = reader;
            this.writer = writer;
        }

    }

    public record Entry(UUID uuid, GameProfile profile, boolean listed, int latency, GameType gameMode,
                        @Nullable Component displayName, @Nullable RemoteChatSession.Data chatSession) {
        Entry(@NotNull NPCServerPlayer npc) {
            this(npc.getUUID(), npc.getGameProfile(), true, npc.latency, npc.gameMode.getGameModeForPlayer(), npc.getTabListDisplayName(), Optionull.map(npc.getChatSession(), RemoteChatSession::asData));
        }
    }

    public interface Reader {
        void read(EntryBuilder entryBuilder, FriendlyByteBuf buffer);
    }

    public interface Writer {
        void write(FriendlyByteBuf buffer, Entry entry);
    }

    public static class EntryBuilder {
        final UUID profileId;
        GameProfile profile;
        boolean listed;
        int latency;
        GameType gameMode = GameType.DEFAULT_MODE;
        @Nullable
        Component displayName;
        @Nullable
        RemoteChatSession.Data chatSession;

        EntryBuilder(UUID profileId) {
            this.profileId = profileId;
            this.profile = new GameProfile(profileId, null);
        }

        Entry build() {
            return new Entry(this.profileId, this.profile, this.listed, this.latency, this.gameMode, this.displayName, this.chatSession);
        }
    }
}
