package net.minecraft.server.network;

import net.minecraft.network.protocol.Packet;

import net.minecraft.network.protocol.game.PacketPlayOutPosition;
import net.minecraft.world.entity.PositionMoveRotation;
import net.minecraft.world.entity.RelativeMovement;
import net.minecraft.world.phys.Vec3D;

import com.bergerkiller.bukkit.common.protocol.CommonPacket;

class PlayerConnection {
    public Object getNetworkManager() {
#if version >= 1.20.2
        // Moved to ServerCommonPacketListenerImpl
        #require net.minecraft.server.network.ServerCommonPacketListenerImpl protected final net.minecraft.network.NetworkManager networkManager:connection;
#elseif version >= 1.17
        #require PlayerConnection private final net.minecraft.network.NetworkManager networkManager:connection;
#else
        #require PlayerConnection public final net.minecraft.network.NetworkManager networkManager;
#endif
        return instance#networkManager;
    }

#if version >= 1.18
    public void sendPacket:send((Object) Packet<?> packet);
#elseif version >= 1.9
    public void sendPacket((Object) Packet<?> packet);
#else
    public void sendPacket((Object) Packet packet);
#endif

    <code>
    private static final QueuePacketMethod defaultQueuePacketMethod = com.bergerkiller.generated.net.minecraft.network.NetworkManagerHandle::queuePacketUnsafe;
    private static final java.util.Map<Class<?>, QueuePacketMethod> queuePacketMethods = new java.util.concurrent.ConcurrentHashMap<Class<?>, QueuePacketMethod>(5, 0.75f, 2);

    private static interface QueuePacketMethod {
        boolean queuePacket(Object networkManager, Object packet);
    }

    static {
        // Default method when the NetworkManager is vanilla unchanged
        queuePacketMethods.put(com.bergerkiller.generated.net.minecraft.network.NetworkManagerHandle.T.getType(), defaultQueuePacketMethod);
    }

    private static QueuePacketMethod findPacketMethod(Class<?> networkManagerType) throws Throwable {
        String typeName = networkManagerType.getName();

        // Denizens
        if (typeName.startsWith("com.denizenscript.denizen.nms.") && typeName.endsWith("DenizenNetworkManagerImpl")) {
            final com.bergerkiller.mountiplex.reflection.util.FastField<Object> oldManagerField = new com.bergerkiller.mountiplex.reflection.util.FastField<Object>();
            oldManagerField.init(networkManagerType.getDeclaredField("oldManager"));
            oldManagerField.forceInitialization();
            return (networkManager, packet) -> {
                Object oldManager = oldManagerField.get(networkManager);
                return queuePacket(oldManager, packet);
            };
        } else if (typeName.startsWith("com.denizenscript.denizen.nms.") && typeName.endsWith("FakeNetworkManagerImpl")) {
            // No base implementation, we can treat it like a vanilla one
            return defaultQueuePacketMethod;
        }

        // Unsupported
        return null;
    }

    private static boolean queuePacket(Object networkManager, Object packet) {
        if (networkManager != null) {
            QueuePacketMethod method = queuePacketMethods.get(networkManager.getClass());
            if (method == null) {
                try {
                    method = findPacketMethod(networkManager.getClass());
                } catch (Throwable t) {
                    // Ignore, assume it isn't supported
                    //t.printStackTrace();
                }
                if (method != null) {
                    queuePacketMethods.put(networkManager.getClass(), method);
                } else {
                    queuePacketMethods.put(networkManager.getClass(), (n, p) -> {return false;});
                    com.bergerkiller.bukkit.common.Logging.LOGGER_NETWORK.warning("Unsupported NetworkManager detected: " + networkManager.getClass().getName());
                    return false;
                }
            }
            if (method.queuePacket(networkManager, packet)) {
                return true;
            }
        }

        return false;
    }

    public void queuePacket(Object packet) {
        if (!queuePacket(getNetworkManager(), packet)) {
            // Fallback: execute next tick
            com.bergerkiller.bukkit.common.utils.CommonUtil.nextTick(() -> sendPacket(packet));
        }
    }
    </code>

    <code>
    public boolean isConnected() {
        return com.bergerkiller.generated.net.minecraft.network.NetworkManagerHandle.T.isConnected.invoke(getNetworkManager()).booleanValue();
    }

    /**
     * Gets the PlayerConnection NMS instance, which is used for sending packets to.
     * If the player is an NPC, or is disconnected, this method returns null.
     * 
     * @param player
     * @return player connection
     */
    public static PlayerConnectionHandle forPlayer(org.bukkit.entity.Player player) {
        Object handle = com.bergerkiller.bukkit.common.conversion.type.HandleConversion.toEntityHandle(player);

        // Check not NPC player
        // This check used to exist 7+ years ago or even older, and even back then it was unknown if this check was even needed.
        // I've disabled it (9 apr 2024) because this breaks sending packets when mods/plugins alter the server player instance.
        //if (!com.bergerkiller.generated.net.minecraft.server.level.EntityPlayerHandle.T.isType(handle)) return null;

        final PlayerConnectionHandle connection = com.bergerkiller.generated.net.minecraft.server.level.EntityPlayerHandle.T.playerConnection.get(handle);
        if (connection == null || !connection.isConnected()) {
            return null; // No PlayerConnection instance or not connected
        }
        return connection;
    }
    </code>

    public void sendPos(double x, double y, double z) {
        java.util.Set flags = new java.util.HashSet();
        flags.add(RelativeMovement.X_ROT);
        flags.add(RelativeMovement.Y_ROT);
  #if version >= 1.21.2
        instance.send((Packet) new PacketPlayOutPosition(0, new PositionMoveRotation(
            new Vec3D(x, y, z), new Vec3D(0.0, 0.0, 0.0), 0.0f, 0.0f
        ), flags));
  #elseif version >= 1.19.4
        instance.send((Packet) new PacketPlayOutPosition(x, y, z, 0.0f, 0.0f, flags, 0));
  #elseif version >= 1.18
        instance.send((Packet) new PacketPlayOutPosition(x, y, z, 0.0f, 0.0f, flags, 0, false));
  #elseif version >= 1.17
        instance.sendPacket((Packet) new PacketPlayOutPosition(x, y, z, 0.0f, 0.0f, flags, 0, false));
  #elseif version >= 1.9
        instance.sendPacket((Packet) new PacketPlayOutPosition(x, y, z, 0.0f, 0.0f, flags, 0));
  #else
        instance.sendPacket((Packet) new PacketPlayOutPosition(x, y, z, 0.0f, 0.0f, flags));
  #endif
    }

    public void resetAwaitTeleport() {
#if version >= 1.9
  #if version >= 1.17
        #require PlayerConnection private net.minecraft.world.phys.Vec3D awaitingPositionFromClient;
        #require PlayerConnection private int awaitingTeleport;
  #else
        #require PlayerConnection private net.minecraft.world.phys.Vec3D awaitingPositionFromClient:teleportPos;
        #require PlayerConnection private int awaitingTeleport:teleportAwait;
  #endif

        net.minecraft.world.phys.Vec3D curr = instance#awaitingPositionFromClient;
        if (curr == null) {
            return;
        }

        int counter = instance#awaitingTeleport;
        if (++counter == Integer.MAX_VALUE) {
            counter = 0;
        }
        instance#awaitingTeleport = counter;
        instance#awaitingPositionFromClient = null;
#endif
    }
}
