package net.minecraft.server.network;

import com.mojang.authlib.GameProfile;
import com.mojang.logging.LogUtils;
import java.util.List;
import java.util.Queue;
import java.util.concurrent.ConcurrentLinkedQueue;
import javax.annotation.Nullable;
import net.minecraft.core.LayeredRegistryAccess;
import net.minecraft.network.Connection;
import net.minecraft.network.DisconnectionDetails;
import net.minecraft.network.RegistryFriendlyByteBuf;
import net.minecraft.network.TickablePacketListener;
import net.minecraft.network.chat.Component;
import net.minecraft.network.protocol.PacketUtils;
import net.minecraft.network.protocol.common.ClientboundCustomPayloadPacket;
import net.minecraft.network.protocol.common.ClientboundDisconnectPacket;
import net.minecraft.network.protocol.common.ClientboundServerLinksPacket;
import net.minecraft.network.protocol.common.ServerboundClientInformationPacket;
import net.minecraft.network.protocol.common.ServerboundResourcePackPacket;
import net.minecraft.network.protocol.common.custom.BrandPayload;
import net.minecraft.network.protocol.configuration.ClientboundUpdateEnabledFeaturesPacket;
import net.minecraft.network.protocol.configuration.ServerConfigurationPacketListener;
import net.minecraft.network.protocol.configuration.ServerboundFinishConfigurationPacket;
import net.minecraft.network.protocol.configuration.ServerboundSelectKnownPacks;
import net.minecraft.network.protocol.game.GameProtocols;
import net.minecraft.server.MinecraftServer;
import net.minecraft.server.RegistryLayer;
import net.minecraft.server.ServerLinks;
import net.minecraft.server.level.ClientInformation;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.server.network.config.JoinWorldTask;
import net.minecraft.server.network.config.ServerResourcePackConfigurationTask;
import net.minecraft.server.network.config.SynchronizeRegistriesTask;
import net.minecraft.server.packs.repository.KnownPack;
import net.minecraft.server.players.PlayerList;
import net.minecraft.world.flag.FeatureFlags;
import org.slf4j.Logger;

public class ServerConfigurationPacketListenerImpl extends ServerCommonPacketListenerImpl implements ServerConfigurationPacketListener, TickablePacketListener {
    private static final Logger LOGGER = LogUtils.getLogger();
    private static final Component DISCONNECT_REASON_INVALID_DATA = Component.translatable("multiplayer.disconnect.invalid_player_data");
    private final GameProfile gameProfile;
    private final Queue<ConfigurationTask> configurationTasks = new ConcurrentLinkedQueue<>();
    @Nullable
    public ConfigurationTask currentTask;
    public ClientInformation clientInformation;
    @Nullable
    private SynchronizeRegistriesTask synchronizeRegistriesTask;
    public io.papermc.paper.connection.PaperPlayerConfigurationConnection paperConnection; // Paper
    public ServerPlayer switchToMain; // Folia - region threading - rewrite login process

    public ServerConfigurationPacketListenerImpl(MinecraftServer server, Connection connection, CommonListenerCookie cookie) {
        super(server, connection, cookie);
        this.gameProfile = cookie.gameProfile();
        this.clientInformation = cookie.clientInformation();
        this.paperConnection = new io.papermc.paper.connection.PaperPlayerConfigurationConnection(this); // Paper
    }

    // Paper start - configuration phase API
    @Override
    public io.papermc.paper.connection.PlayerCommonConnection getApiConnection() {
        return this.paperConnection;
    }

    @Override
    public net.kyori.adventure.audience.Audience getAudience() {
        return this.paperConnection.getAudience();
    }
    // Paper end - configuration phase API

    @Override
    protected GameProfile playerProfile() {
        return this.gameProfile;
    }

    @Override
    public void onDisconnect(DisconnectionDetails details) {
        // Paper start - Debugging
        if (this.server.isDebugging()) {
            ServerConfigurationPacketListenerImpl.LOGGER.info("{} lost connection: {}, while in configuration phase {}", this.gameProfile.getName(), details.reason().getString(), this.currentTask != null ? this.currentTask.type().id() : "null");
        } else
        // Paper end
        LOGGER.info("{} lost connection: {}", this.gameProfile.getName(), details.reason().getString()); // Paper - log name instead of whole profile (as with other similar messages)
        super.onDisconnect(details);
    }

    @Override
    public boolean isAcceptingMessages() {
        return this.connection.isConnected();
    }

    public void startConfiguration() {
        new io.papermc.paper.event.connection.configuration.PlayerConnectionInitialConfigureEvent(this.paperConnection).callEvent(); // Paper
        this.send(new ClientboundCustomPayloadPacket(new BrandPayload(this.server.getServerModName())));
        ServerLinks serverLinks = this.server.serverLinks();
        if (!serverLinks.isEmpty()) {
            // Paper start
            org.bukkit.craftbukkit.CraftServerLinks links = new org.bukkit.craftbukkit.CraftServerLinks(serverLinks);
            new org.bukkit.event.player.PlayerLinksSendEvent(this.paperConnection, links).callEvent();
            this.send(new ClientboundServerLinksPacket(links.getServerLinks().untrust()));
            // Paper end
        }

        LayeredRegistryAccess<RegistryLayer> layeredRegistryAccess = this.server.registries();
        List<KnownPack> list = this.server
            .getResourceManager()
            .listPacks()
            .flatMap(packResources -> packResources.location().knownPackInfo().stream())
            .toList();
        this.send(new ClientboundUpdateEnabledFeaturesPacket(FeatureFlags.REGISTRY.toNames(this.server.getWorldData().enabledFeatures())));
        this.synchronizeRegistriesTask = new SynchronizeRegistriesTask(list, layeredRegistryAccess);
        this.configurationTasks.add(this.synchronizeRegistriesTask);
        this.addOptionalTasks();
        this.configurationTasks.add(new io.papermc.paper.connection.PaperConfigurationTask(this)); // Paper
        this.configurationTasks.add(new JoinWorldTask());
        this.startNextTask();
    }

    public void returnToWorld() {
        this.configurationTasks.add(new JoinWorldTask());
        this.startNextTask();
    }

    private void addOptionalTasks() {
        this.server
            .getServerResourcePack()
            .ifPresent(serverResourcePackInfo -> this.configurationTasks.add(new ServerResourcePackConfigurationTask(serverResourcePackInfo)));
    }

    @Override
    public void handleClientInformation(ServerboundClientInformationPacket packet) {
        this.clientInformation = packet.information();
        this.connection.channel.attr(io.papermc.paper.adventure.PaperAdventure.LOCALE_ATTRIBUTE).set(net.kyori.adventure.translation.Translator.parseLocale(packet.information().language())); // Paper
    }

    @Override
    public void handleResourcePackResponse(ServerboundResourcePackPacket packet) {
        super.handleResourcePackResponse(packet);
        this.connection.resourcePackStatus = org.bukkit.event.player.PlayerResourcePackStatusEvent.Status.values()[packet.action().ordinal()]; // Paper
        if (packet.action().isTerminal() && packet.id().equals(this.server.getServerResourcePack().map(MinecraftServer.ServerResourcePackInfo::id).orElse(null))) { // Paper - Ignore resource pack requests that are not vanilla
            this.finishCurrentTask(ServerResourcePackConfigurationTask.TYPE);
        }
    }

    @Override
    public void handleSelectKnownPacks(ServerboundSelectKnownPacks packet) {
        PacketUtils.ensureRunningOnSameThread(packet, this, this.server);
        if (this.synchronizeRegistriesTask == null) {
            throw new IllegalStateException("Unexpected response from client: received pack selection, but no negotiation ongoing");
        } else {
            this.synchronizeRegistriesTask.handleResponse(packet.knownPacks(), this::send);
            this.finishCurrentTask(SynchronizeRegistriesTask.TYPE);
        }
    }

    @Override
    public void handleConfigurationFinished(ServerboundFinishConfigurationPacket packet) {
        PacketUtils.ensureRunningOnSameThread(packet, this, this.server);
        this.finishCurrentTask(JoinWorldTask.TYPE);
        // this.connection.setupOutboundProtocol(GameProtocols.CLIENTBOUND_TEMPLATE.bind(RegistryFriendlyByteBuf.decorator(this.server.registryAccess()))); // Luminol - Async protocol switch - move down

        Runnable afterSwitch = () -> { // Luminol - Async protocol switch
        try {
            PlayerList playerList = this.server.getPlayerList();
            if (playerList.getPlayer(this.gameProfile.getId()) != null) {
                this.disconnect(PlayerList.DUPLICATE_LOGIN_DISCONNECT_MESSAGE);
                return;
            }

            Component component = org.bukkit.craftbukkit.event.CraftEventFactory.handleLoginResult(playerList.canPlayerLogin(this.connection.getRemoteAddress(), this.gameProfile, this.connection), this.paperConnection, this.connection, this.gameProfile, this.server, false); // Paper - Login event logic // Folia - region threading - add connection parameter
            if (component != null) {
                this.disconnect(component);
                return;
            }

            // Paper start
            ServerPlayer serverPlayer;
            if (this.connection.savedPlayerForLoginEventLegacy != null) {
                serverPlayer = this.connection.savedPlayerForLoginEventLegacy;
                // WE have to do this because the player isnt updated properly
                serverPlayer.updateOptionsNoEvents(this.clientInformation);
            } else {
                serverPlayer = new ServerPlayer(this.server, this.server.overworld(), this.gameProfile, this.clientInformation);
            }
            // Paper end
            // Folia start - region threading - rewrite login process
            io.papermc.paper.threadedregions.RegionizedServer.ensureGlobalTickThread("Cannot handle player login off global tick thread");
            CommonListenerCookie clientData = this.createCookie(this.clientInformation);
            org.apache.commons.lang3.mutable.MutableObject<net.minecraft.util.ProblemReporter.ScopedCollector> scopedCollector = new org.apache.commons.lang3.mutable.MutableObject<>();
            org.apache.commons.lang3.mutable.MutableObject<net.minecraft.world.level.storage.ValueInput> data = new org.apache.commons.lang3.mutable.MutableObject<>();
            org.apache.commons.lang3.mutable.MutableObject<String> lastKnownName = new org.apache.commons.lang3.mutable.MutableObject<>();
            ca.spottedleaf.concurrentutil.completable.CallbackCompletable<org.bukkit.Location> toComplete = new ca.spottedleaf.concurrentutil.completable.CallbackCompletable<>();
            // note: need to call addWaiter before completion to ensure the callback is invoked synchronously
            // the loadSpawnForNewPlayer function always completes the completable once the chunks were loaded,
            // on the load callback for those chunks (so on the same region)
            // this guarantees the chunk cannot unload under our feet
            toComplete.addWaiter((org.bukkit.Location loc, Throwable t) -> {
                int chunkX = net.minecraft.util.Mth.floor(loc.getX()) >> 4;
                int chunkZ = net.minecraft.util.Mth.floor(loc.getZ()) >> 4;

                net.minecraft.server.level.ServerLevel world = ((org.bukkit.craftbukkit.CraftWorld)loc.getWorld()).getHandle();
                // we just need to hold the chunks at loaded until the next tick
                // so we do not need to care about unique IDs for the ticket
                world.moonrise$getChunkTaskScheduler().chunkHolderManager.addTicketAtLevel(
                    net.minecraft.server.level.TicketType.LOGIN,
                    new net.minecraft.world.level.ChunkPos(chunkX, chunkZ),
                    ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkHolderManager.FULL_LOADED_TICKET_LEVEL,
                    null
                );

                io.papermc.paper.threadedregions.RegionizedServer.getInstance().taskQueue.queueTickTaskQueue(
                    world, chunkX, chunkZ,
                    () -> {
                        // once switchToMain is set, the current ticking region now owns the connection and is responsible
                        // for cleaning it up
                        playerList.placeNewPlayer(
                            ServerConfigurationPacketListenerImpl.this.connection,
                            serverPlayer,
                            clientData,
                            scopedCollector.getValue(),
                            java.util.Optional.ofNullable(data.getValue()),
                            lastKnownName.getValue(),
                            loc
                        );
                    },
                    ca.spottedleaf.concurrentutil.util.Priority.HIGHER
                );
            });
            this.switchToMain = serverPlayer;
            try {
                // now the connection responsibility is transferred on the region
                playerList.loadSpawnForNewPlayer(this.connection, serverPlayer, clientData, scopedCollector, data, lastKnownName, toComplete);
            } catch (final Throwable throwable) {
                // assume toComplete will not be invoked
                // ensure global tick thread owns the connection again, to properly disconnect it
                this.switchToMain = null;
                throw new RuntimeException(throwable);
            }
            // Folia end - region threading - rewrite login process
        } catch (Exception var5) {
            LOGGER.error("Couldn't place player in world", (Throwable)var5);
            // Paper start - Debugging
            if (this.server.isDebugging()) {
                var5.printStackTrace();
            }
            // Paper end - Debugging
            this.connection.send(new ClientboundDisconnectPacket(DISCONNECT_REASON_INVALID_DATA));
            this.connection.disconnect(DISCONNECT_REASON_INVALID_DATA);
        }
        // Luminol start - Async protocol switch
        };
        if (!me.earthme.luminol.config.modules.optimizations.AsyncProtocolChangeConfig.enabled) {
            this.connection.setupOutboundProtocol(GameProtocols.CLIENTBOUND_TEMPLATE.bind(RegistryFriendlyByteBuf.decorator(this.server.registryAccess())));
            afterSwitch.run(); // directly run callback as we won't process any packet this time
        } else {
            this.connection.setupOutboundProtocolAsync(GameProtocols.CLIENTBOUND_TEMPLATE.bind(RegistryFriendlyByteBuf.decorator(this.server.registryAccess())), () -> {
                // push back
                io.papermc.paper.threadedregions.RegionizedServer.getInstance().addTask(afterSwitch);
            }, false); // we will start auto read once we set up inbound handler at placeNewPlayer in PlayerList
        }
        // Luminol end
    }

    @Override
    public void tick() {
        this.keepConnectionAlive();
    }

    private void startNextTask() {
        if (this.currentTask != null) {
            throw new IllegalStateException("Task " + this.currentTask.type().id() + " has not finished yet");
        } else if (this.isAcceptingMessages()) {
            ConfigurationTask configurationTask = this.configurationTasks.poll();
            if (configurationTask != null) {
                this.currentTask = configurationTask;
                configurationTask.start(this::send);
            }
        }
    }

    public void finishCurrentTask(ConfigurationTask.Type taskType) {
        ConfigurationTask.Type type = this.currentTask != null ? this.currentTask.type() : null;
        if (!taskType.equals(type)) {
            throw new IllegalStateException("Unexpected request for task finish, current task: " + type + ", requested: " + taskType);
        } else {
            this.currentTask = null;
            this.startNextTask();
        }
    }

    // Paper start
    @Override
    public void disconnectAsync(final net.minecraft.network.DisconnectionDetails disconnectionInfo) {
        if (io.papermc.paper.threadedregions.RegionizedServer.isGlobalTickThread()) { // Luminol - Fix unpatched task returning of ServerConfigurationPacketListenerImpl#disconnectAsync
            this.disconnect(disconnectionInfo);
            return;
        }

        this.connection.setReadOnly();
        // this.server.scheduleOnMain(() -> { // Luminol - Fix unpatched task returning of ServerConfigurationPacketListenerImpl#disconnectAsync
        io.papermc.paper.threadedregions.RegionizedServer.getInstance().addTask(() -> { // Luminol - Fix unpatched task returning of ServerConfigurationPacketListenerImpl#disconnectAsync
            this.disconnect(disconnectionInfo); // Currently you cannot cancel disconnect during the config stage
        });
    }

    @Override
    public void handleCustomPayload(net.minecraft.network.protocol.common.ServerboundCustomPayloadPacket packet) {
        super.handleCustomPayload(packet);
    }

    @Override
    public io.papermc.paper.connection.PaperPlayerConfigurationConnection paperConnection() {
        return paperConnection;
    }
    // Paper end
}
