package net.minecraft.server.level;

import net.minecraft.server.network.PlayerConnection;
import net.minecraft.network.protocol.game.PacketPlayOutLightUpdate;
import net.minecraft.network.protocol.game.PacketPlayOutMapChunk;
import net.minecraft.world.entity.Entity;
import net.minecraft.world.level.chunk.Chunk;
import net.minecraft.world.level.ChunkCoordIntPair;
import net.minecraft.world.level.lighting.LightEngine;
import net.minecraft.world.level.lighting.LevelLightEngine;

import com.bergerkiller.bukkit.common.bases.IntVector2;
import com.bergerkiller.bukkit.common.bases.IntVector3;

import com.bergerkiller.generated.net.minecraft.server.level.EntityPlayerHandle;
import com.bergerkiller.generated.net.minecraft.server.level.PlayerChunkMapHandle;
import com.bergerkiller.generated.net.minecraft.server.level.PlayerChunkHandle;
import com.bergerkiller.generated.net.minecraft.world.level.WorldHandle;

import io.github.opencubicchunks.cubicchunks.api.world.ICubicWorld;
import io.github.opencubicchunks.cubicchunks.api.world.IColumn;
import io.github.opencubicchunks.cubicchunks.api.world.ICube;
import io.github.opencubicchunks.cubicchunks.api.world.ICubeWatcher;

class PlayerChunk {
#if version >= 1.17
    #require net.minecraft.server.level.EntityPlayer public net.minecraft.server.network.PlayerConnection playerConnection:connection;
#else
    #require net.minecraft.server.level.EntityPlayer public net.minecraft.server.network.PlayerConnection playerConnection;
#endif

    public (PlayerChunkMapHandle) PlayerChunkMap getPlayerChunkMap() {
#if version >= 1.17
        return (PlayerChunkMap) instance.playerProvider;
#elseif version >= 1.14
        return (PlayerChunkMap) instance.players;
#elseif exists net.minecraft.server.level.PlayerChunk private final PlayerChunkMap playerChunkMap;
        #require net.minecraft.server.level.PlayerChunk private final PlayerChunkMap playerChunkMap;
        return instance#playerChunkMap;
#elseif exists net.minecraft.server.level.PlayerChunk private final PlayerChunkMap this$0;
        #require net.minecraft.server.level.PlayerChunk private final PlayerChunkMap playerChunkMap:this$0;
        return instance#playerChunkMap;
#else
        #require net.minecraft.server.level.PlayerChunk final PlayerChunkMap playerChunkMap;
        return instance#playerChunkMap;
#endif
    }

#if version >= 1.20
    #require net.minecraft.server.level.PlayerChunk private final net.minecraft.world.level.lighting.LevelLightEngine lightEngineField:lightEngine;
#elseif version >= 1.16
    #require net.minecraft.server.level.PlayerChunk private final net.minecraft.world.level.lighting.LightEngine lightEngineField:lightEngine;
#endif

    public boolean resendChunk() {
        Chunk chunk = (Chunk) ((com.bergerkiller.mountiplex.reflection.declarations.Template$Method) PlayerChunkHandle.T.getChunkIfLoaded.raw).invoke(instance);
        if (chunk == null) {
            return false;
        }

        // Cubic chunks support
#if exists io.github.opencubicchunks.cubicchunks.api.world.ICube
        if (chunk.getWorld() instanceof ICubicWorld && ((ICubicWorld) chunk.getWorld()).isCubicWorld()) {
            // Retrieve cube map
            PlayerChunkMap playerChunkMap = (PlayerChunkMap) ((com.bergerkiller.mountiplex.reflection.declarations.Template$Method) PlayerChunkHandle.T.getPlayerChunkMap.raw).invoke(instance);
            io.github.opencubicchunks.cubicchunks.core.server.PlayerCubeMap playerCubeMap;
            playerCubeMap = (io.github.opencubicchunks.cubicchunks.core.server.PlayerCubeMap) playerChunkMap;

            // Query cubes in chunk column
            java.util.Iterator cubes_iter = ((IColumn) chunk).getLoadedCubes().iterator();

            // For all loaded cubes, find the CubeWatcher
            // If it exists, resend lighting to the players that see it
            boolean sentChanges = false;
            while (cubes_iter.hasNext()) {
                ICube cube = (ICube) cubes_iter.next();
                ICubeWatcher cubeWatcher = playerCubeMap.getCubeWatcher(cube.getCoords());
                if (cubeWatcher == null || !cubeWatcher.isSentToPlayers()) {
                    continue;
                }

                // Gather list of players to send this cube to using reflection (there's no getter)
                #require io.github.opencubicchunks.cubicchunks.core.server.CubeWatcher private final readonly it.unimi.dsi.fastutil.objects.ObjectArrayList<net.minecraft.server.level.EntityPlayer> players;
                it.unimi.dsi.fastutil.objects.ObjectArrayList obj_players = cubeWatcher#players;
                Object[] arr_players = obj_players.elements();

                // Send cube to all players that see it according to the watcher
                int num_players = arr_players.length;
                for (int i = 0; i < num_players; i++) {
                    sentChanges = true;
                    EntityPlayer player = (EntityPlayer) arr_players[i];
                    if (player != null) {
                        playerCubeMap.scheduleSendCubeToPlayer((io.github.opencubicchunks.cubicchunks.core.world.cube.Cube) cube, player);
                    }
                }
            }

            // Don't do any of the vanilla code!
            return sentChanges;
        }
#endif

        Collection players = (Collection) ((com.bergerkiller.mountiplex.reflection.declarations.Template$Method) PlayerChunkHandle.T.getPlayers.raw).invoke(instance);
        if (players.isEmpty()) {
            return false;
        }

        // Create a map chunk packet
#if version >= 1.20
        // Note: is now ClientboundLevelChunkWithLightPacket
        LevelLightEngine lightengine = instance#lightEngineField;
        PacketPlayOutMapChunk packet = new PacketPlayOutMapChunk(chunk, lightengine, null, null);
#elseif version >= 1.18
        // Note: is now ClientboundLevelChunkWithLightPacket
        LightEngine lightengine = instance#lightEngineField;
        PacketPlayOutMapChunk packet = new PacketPlayOutMapChunk(chunk, lightengine, null, null, true);
#elseif version >= 1.17
        PacketPlayOutMapChunk packet = new PacketPlayOutMapChunk(chunk);
#elseif version >= 1.16 && version <= 1.16.1
        PacketPlayOutMapChunk packet = new PacketPlayOutMapChunk(chunk, 0x1FFFF, true);
#elseif exists net.minecraft.network.protocol.game.PacketPlayOutMapChunk public PacketPlayOutMapChunk(net.minecraft.world.level.chunk.Chunk chunk, int sectionsMask);
        PacketPlayOutMapChunk packet = new PacketPlayOutMapChunk(chunk, 0x1FFFF);
#else
        boolean flag = !WorldHandle.createHandle(chunk.getWorld()).getDimensionType().hasSkyLight();
        PacketPlayOutMapChunk packet = new PacketPlayOutMapChunk(chunk, flag, 0x1FFFF);
#endif

        // Send to all players that see this chunk
        java.util.Iterator iter = players.iterator();
        while (iter.hasNext()) {
            EntityPlayer player = (EntityPlayer) iter.next();
            PlayerConnection connection = player#playerConnection;
            if (connection != null) {
#if version >= 1.18
                connection.send((net.minecraft.network.protocol.Packet) packet);
#else
                connection.sendPacket((net.minecraft.network.protocol.Packet) packet);
#endif
            }
        }

        // Resend lighting information afterwards between 1.14 and 1.17.1
        // After 1.18 light and chunk data are in one packet again, so no need
#if version >= 1.14 && version <= 1.17.1
        PlayerChunkHandle.T.resendAllLighting.invoke(instance);
#endif

        return true;
    }

#if version >= 1.16
    // Since Minecraft 1.16 the sending of lighting is broken and nothing gets updated
    // We must send the light update packet ourselves
    public boolean resendAllLighting() {
  #if version >= 1.20.5
        Chunk chunk = instance.getFullChunkNow();
  #elseif version >= 1.18
        Chunk chunk = instance.getFullChunk();
  #else
        Chunk chunk = instance.getChunk();
  #endif
        if (chunk == null) {
            return false;
        }

        // Check there are players to receive data at all
        Collection players = (Collection) ((com.bergerkiller.mountiplex.reflection.declarations.Template$Method) PlayerChunkHandle.T.getPlayers.raw).invoke(instance);
        if (players.isEmpty()) {
            return false;
        }

        // Reset dirty mask so it does not send twice
  #if version >= 1.17
        #require net.minecraft.server.level.PlayerChunk private final BitSet blockChangedLightSectionFilter;
        #require net.minecraft.server.level.PlayerChunk private final BitSet skyChangedLightSectionFilter;
        BitSet blockFilter = instance#blockChangedLightSectionFilter;
        BitSet skyFilter = instance#skyChangedLightSectionFilter;
        blockFilter.clear();
        skyFilter.clear();
  #else
    #if version >= 1.16.2
        #require net.minecraft.server.level.PlayerChunk private int pc_dirtyBlockLightMask:r;
        #require net.minecraft.server.level.PlayerChunk private int pc_dirtySkyLightMask:s;
    #else
        #require net.minecraft.server.level.PlayerChunk private int pc_dirtyBlockLightMask:s;
        #require net.minecraft.server.level.PlayerChunk private int pc_dirtySkyLightMask:t;
    #endif
        instance#pc_dirtyBlockLightMask = 0;
        instance#pc_dirtySkyLightMask = 0;
  #endif

        // Retrieve LightEngine (used to make the update packet)
  #if version >= 1.20
        LevelLightEngine lightEngine = instance#lightEngineField;
  #else
        LightEngine lightEngine = instance#lightEngineField;
  #endif

        // Create a lighting update packet, similar to how it is done in PlayerChunk
  #if version >= 1.20
        PacketPlayOutLightUpdate packet = new PacketPlayOutLightUpdate(chunk.getPos(), lightEngine, null, null);
  #elseif version >= 1.17
        PacketPlayOutLightUpdate packet = new PacketPlayOutLightUpdate(chunk.getPos(), lightEngine, null, null, false);
  #else
        PacketPlayOutLightUpdate packet = new PacketPlayOutLightUpdate(chunk.getPos(), lightEngine, 0x3FFFF, 0x3FFFF, false);
  #endif

        // Send the packet to all players that can see this player chunk
        java.util.Iterator players_iter = players.iterator();
        while (players_iter.hasNext()) {
            EntityPlayer player = (EntityPlayer) players_iter.next();
            PlayerConnection connection = player#playerConnection;
            if (connection != null) {
  #if version >= 1.18
                connection.send((net.minecraft.network.protocol.Packet) packet);
  #else
                connection.sendPacket((net.minecraft.network.protocol.Packet) packet);
  #endif
            }
        }

        return true;
    }
#elseif version >= 1.14
    public boolean resendAllLighting() {
        Chunk chunk = instance.getChunk();
        if (chunk == null) {
            return false;
        }

        // Check there are players to receive data at all
        Collection players = (Collection) ((com.bergerkiller.mountiplex.reflection.declarations.Template$Method) PlayerChunkHandle.T.getPlayers.raw).invoke(instance);
        if (players.isEmpty()) {
            return false;
        }

  #if version >= 1.14.1
        #require net.minecraft.server.level.PlayerChunk private int pc_layerLightMask:s;
        #require net.minecraft.server.level.PlayerChunk private int pc_dirtyBlockLightMask:t;
        #require net.minecraft.server.level.PlayerChunk private int pc_dirtySkyLightMask:u;
  #else
        #require net.minecraft.server.level.PlayerChunk private int pc_layerLightMask:r;
        #require net.minecraft.server.level.PlayerChunk private int pc_dirtyBlockLightMask:s;
        #require net.minecraft.server.level.PlayerChunk private int pc_dirtySkyLightMask:t;
  #endif
        // Set all these masks to 'all layers' (18) to refresh them
        instance#pc_layerLightMask = 0x3FFFF;
        instance#pc_dirtyBlockLightMask = 0x3FFFF;
        instance#pc_dirtySkyLightMask = 0x3FFFF;

        // Update now
        instance.a(chunk);
        return true;
    }
#else
    public boolean resendAllLighting() {
        // Send entire chunk instead, lighting isn't separate
        return ((Boolean) PlayerChunkHandle.T.resendChunk.invoke(instance)).booleanValue();
    }
#endif

#if version >= 1.18
    public (Collection<org.bukkit.entity.Player>) Collection<EntityPlayer> getPlayers() {
        return instance.playerProvider.getPlayers(instance.getPos(), false);
    }
#elseif version >= 1.17
    public (Collection<org.bukkit.entity.Player>) Collection<EntityPlayer> getPlayers() {
        return instance.playerProvider.a(instance.i(), false).collect(java.util.stream.Collectors.toList());
    }
#elseif version >= 1.14.3
    public (Collection<org.bukkit.entity.Player>) Collection<EntityPlayer> getPlayers() {
        return instance.players.a(instance.i(), false).collect(java.util.stream.Collectors.toList());
    }
#elseif version >= 1.14
    public (Collection<org.bukkit.entity.Player>) Collection<EntityPlayer> getPlayers() {
        return instance.players.a(instance.h(), false).collect(java.util.stream.Collectors.toList());
    }
#else
    public (Collection<org.bukkit.entity.Player>) Collection<EntityPlayer> getPlayers() {
        // Require the player list field
#if version >= 1.9
  #if fieldexists net.minecraft.server.level.PlayerChunk public final List<EntityPlayer> players
        #require net.minecraft.server.level.PlayerChunk public final List<EntityPlayer> pc_playersField:players;
  #else
        #require net.minecraft.server.level.PlayerChunk public final List<EntityPlayer> pc_playersField:c;
  #endif
#else
  #if fieldexists  net.minecraft.server.level.PlayerChunk private final Set<EntityPlayer> b;
        // WineSpigot
        #require net.minecraft.server.level.PlayerChunk private final Set<EntityPlayer> pc_playersField:b;
  #else
        #require net.minecraft.server.level.PlayerChunk private final List<EntityPlayer> pc_playersField:b;
  #endif
#endif
        return instance#pc_playersField;
    }
#endif

#if version >= 1.18
    public (IntVector2) ChunkCoordIntPair getLocation:getPos();
#elseif version >= 1.14.3
    public (IntVector2) ChunkCoordIntPair getLocation:i();
#elseif version >= 1.14
    public (IntVector2) ChunkCoordIntPair getLocation:h();
#elseif version >= 1.9
    public (IntVector2) ChunkCoordIntPair getLocation:a();
#else
    public (IntVector2) ChunkCoordIntPair getLocation() {
        #require PlayerChunk private final net.minecraft.world.level.ChunkCoordIntPair location;
        return instance#location;
    }
#endif

    /*
#if version >= 1.17
    private boolean done:wasAccessibleSinceLastSave;
#elseif version >= 1.14.1
    private boolean done:hasBeenLoaded;
#elseif version >= 1.14
    private boolean done:x;
#elseif version >= 1.9
    private boolean done;
#else
    private boolean done:loaded;
#endif
    */

    // Moved to PlayerChunkMap in 1.14
    //     public void addPlayer:a((org.bukkit.entity.Player) EntityPlayer player);
    //     public void removePlayer:b((org.bukkit.entity.Player) EntityPlayer player);

    // #if version >= 1.9
    //     public void sendChunk((org.bukkit.entity.Player) EntityPlayer player);
    // #else
    //     public void sendChunk:b((org.bukkit.entity.Player) EntityPlayer player);
    // #endif

#if version >= 1.20.5
    //TODO: Is this safe? It's not the exact same code. The old one is just gone, and only
    //      checked the full chunk future. The new one does a lot more.
    public (org.bukkit.Chunk) Chunk getChunkIfLoaded:getFullChunkNow();
#elseif version >= 1.18
    public (org.bukkit.Chunk) Chunk getChunkIfLoaded:getFullChunk();
#elseif version >= 1.14
    public (org.bukkit.Chunk) Chunk getChunkIfLoaded:getChunk();
#elseif version >= 1.9
    public (org.bukkit.Chunk) Chunk getChunkIfLoaded() {
        return instance.chunk;
    }
#elseif version >= 1.8
    #require net.minecraft.server.level.PlayerChunk private final net.minecraft.world.level.ChunkCoordIntPair location;
    #require net.minecraft.server.level.PlayerChunk private boolean loaded;
    #if exists net.minecraft.server.level.PlayerChunk private final PlayerChunkMap this$0;
        #require net.minecraft.server.level.PlayerChunk private final PlayerChunkMap playerChunkMap:this$0;
    #else
        #require net.minecraft.server.level.PlayerChunk final PlayerChunkMap playerChunkMap;
    #endif

    public (org.bukkit.Chunk) Chunk getChunkIfLoaded() {
        boolean loaded = instance#loaded;
        if (loaded) {
            PlayerChunkMap map = instance#playerChunkMap;
            ChunkCoordIntPair loc = instance#location;
            return map.a().chunkProviderServer.getChunkAt(loc.x, loc.z);
        } else {
            return null;
        }
    }
#endif

}

class PlayerChunkMap {
    //    private final (List<org.bukkit.entity.Player>) List<EntityPlayer> managedPlayers;
    //
    //#if version >= 1.9
    //    private final optional (Queue<PlayerChunkHandle>) Queue<PlayerChunk> updateQueue_1_8_8:###;
    //    private final unknown Set<PlayerChunk> dirtyBlockChunks:f;
    //    private final unknown List<PlayerChunk> g;
    //    private final unknown List<PlayerChunk> h;
    //    private final unknown List<PlayerChunk> i;
    //    private int radius:j;
    //#else
    //    private final optional (Queue<PlayerChunkHandle>) Queue<PlayerChunk> updateQueue_1_8_8:e;
    //    private final unknown Queue<PlayerChunk> f;
    //    private int radius:g;
    //#endif

    //#if version >= 1.9
    //    public optional void markForUpdate_1_10_2:a((PlayerChunkHandle) PlayerChunk playerchunk);
    //#else
    //    public optional void markForUpdate_1_10_2:###((PlayerChunkHandle) PlayerChunk playerchunk);
    //#endif
    //    <code>
    //    public void markForUpdate(PlayerChunkHandle playerChunk) {
    //        if (T.markForUpdate_1_10_2.isAvailable()) {
    //            T.markForUpdate_1_10_2.invoke(getRaw(), playerChunk);
    //        } else {
    //            T.updateQueue_1_8_8.get(getRaw()).add(playerChunk);
    //        }
    //    }
    //    </code>
    //
    //    private boolean shouldUnload:a(int i, int j, int k, int l, int i1);

#if version >= 1.14
  #if version >= 1.18
    #require net.minecraft.server.level.PlayerChunkMap protected (Object) PlayerChunk getVisibleChunk_1_14:getVisibleChunkIfPresent(long i);
    #require net.minecraft.server.level.PlayerChunkMap protected (Object) PlayerChunk getUpdatingChunk_1_14:getUpdatingChunkIfPresent(long i);

    public (PlayerChunkHandle) PlayerChunk getVisibleChunk(int x, int z) {
        return (PlayerChunk) instance#getVisibleChunk_1_14(ChunkCoordIntPair.asLong(x, z));
    }
    public (PlayerChunkHandle) PlayerChunk getUpdatingChunk(int x, int z) {
        return (PlayerChunk) instance#getUpdatingChunk_1_14(ChunkCoordIntPair.asLong(x, z));
    }
  #else
    #require net.minecraft.server.level.PlayerChunkMap protected (Object) PlayerChunk getVisibleChunk_1_14:getVisibleChunk(long i);
    #require net.minecraft.server.level.PlayerChunkMap protected (Object) PlayerChunk getUpdatingChunk_1_14:getUpdatingChunk(long i);

    public (PlayerChunkHandle) PlayerChunk getVisibleChunk(int x, int z) {
        return (PlayerChunk) instance#getVisibleChunk_1_14(ChunkCoordIntPair.pair(x, z));
    }
    public (PlayerChunkHandle) PlayerChunk getUpdatingChunk(int x, int z) {
        return (PlayerChunk) instance#getUpdatingChunk_1_14(ChunkCoordIntPair.pair(x, z));
    }
  #endif
#elseif version >= 1.9.4
    public (PlayerChunkHandle) PlayerChunk getVisibleChunk:getChunk(int x, int z);
    public (PlayerChunkHandle) PlayerChunk getUpdatingChunk:getChunk(int x, int z);
#elseif version >= 1.9
    public (PlayerChunkHandle) PlayerChunk getVisibleChunk:b(int x, int z);
    public (PlayerChunkHandle) PlayerChunk getUpdatingChunk:b(int x, int z);
#else
    #require net.minecraft.server.level.PlayerChunkMap private PlayerChunk getChunk_1_8_8:a(int x, int z, boolean create);
    public (PlayerChunkHandle) PlayerChunk getVisibleChunk(int x, int z) {
        return instance#getChunk_1_8_8(x, z, false);
    }
    public (PlayerChunkHandle) PlayerChunk getUpdatingChunk(int x, int z) {
        return instance#getChunk_1_8_8(x, z, false);
    }
#endif

<code>
    public java.util.Collection<org.bukkit.entity.Player> getChunkEnteredPlayers(int chunkX, int chunkZ) {
        PlayerChunkHandle playerChunk = getVisibleChunk(chunkX, chunkZ);
        if (playerChunk == null || playerChunk.getChunkIfLoaded() == null) {
            return java.util.Collections.emptyList();
        } else {
            return playerChunk.getPlayers();
        }
    }
</code>

#if version >= 1.14
    public boolean isChunkEntered((EntityPlayerHandle) EntityPlayer entityplayer, int chunkX, int chunkZ) {
  #if version >= 1.18
        long key = ChunkCoordIntPair.asLong(chunkX, chunkZ);
  #else
        long key = ChunkCoordIntPair.pair(chunkX, chunkZ);
  #endif

  #if exists net.minecraft.server.level.PlayerChunkMap public PlayerChunk getVisibleChunkIfPresent(long key);
        PlayerChunk chunk = instance.getVisibleChunkIfPresent(key);
  #elseif exists net.minecraft.server.level.PlayerChunkMap public PlayerChunk getVisibleChunk(long key);
        PlayerChunk chunk = instance.getVisibleChunk(key);
  #elseif version >= 1.17
        PlayerChunk chunk = (PlayerChunk) instance.visibleChunkMap.get(key);
  #else
        PlayerChunk chunk = (PlayerChunk) instance.visibleChunks.get(key);
  #endif

  #if version >= 1.20.5
        if (chunk == null || chunk.getFullChunkNow() == null) {
  #elseif version >= 1.18
        if (chunk == null || chunk.getFullChunk() == null) {
  #else
        if (chunk == null || chunk.getChunk() == null) {
  #endif
            return false;
        }

        // Check Stream contains Player
        ChunkCoordIntPair chunkCoordinates = new ChunkCoordIntPair(chunkX, chunkZ);
  #if version >= 1.18
        java.util.List players = chunk.playerProvider.getPlayers(chunkCoordinates, false);
        return players.contains(entityplayer);
  #elseif version >= 1.17
        java.util.stream.Stream players = chunk.playerProvider.a(chunkCoordinates, false);
        return players.anyMatch(java.util.function.Predicate.isEqual(entityplayer));
  #else
        java.util.stream.Stream players = chunk.players.a(chunkCoordinates, false);
        return players.anyMatch(java.util.function.Predicate.isEqual(entityplayer));
  #endif
    }
#else
    public boolean isChunkEntered:a((EntityPlayerHandle) EntityPlayer entityplayer, int chunkX, int chunkZ);
#endif

// Needed to make sure the EntityTracker hook works
#if exists net.minecraft.server.level.PlayerChunkMap public void addEntity(net.minecraft.world.entity.Entity);
    public optional void trackEntity:addEntity((org.bukkit.entity.Entity) Entity entity);
#elseif version >= 1.14
    protected optional void trackEntity:addEntity((org.bukkit.entity.Entity) Entity entity);
#else
    protected optional void trackEntity:###((org.bukkit.entity.Entity) Entity entity);
#endif

}