package net.minecraft.world.level.chunk;

import net.minecraft.core.BlockPosition;
import net.minecraft.core.SectionPosition;
import net.minecraft.server.level.WorldServer;
import net.minecraft.world.entity.Entity;
import net.minecraft.world.level.chunk.ChunkSection;
import net.minecraft.world.level.EnumSkyBlock;
import net.minecraft.world.level.block.state.IBlockData;
import net.minecraft.world.level.levelgen.HeightMap;
import net.minecraft.world.level.levelgen.HeightMap$Type;
import net.minecraft.world.level.World;
import net.minecraft.world.level.entity.EntitySection;
import net.minecraft.world.level.entity.EntitySectionStorage;

import com.bergerkiller.bukkit.common.wrappers.BlockData;
import com.bergerkiller.bukkit.common.bases.IntVector3;
import com.bergerkiller.bukkit.common.collections.List2D;

import com.bergerkiller.generated.net.minecraft.world.entity.EntityHandle;
import com.bergerkiller.generated.net.minecraft.world.level.chunk.ChunkHandle;
import com.bergerkiller.generated.net.minecraft.world.level.chunk.ChunkSectionHandle;
import com.bergerkiller.generated.net.minecraft.world.level.EnumSkyBlockHandle;

import com.bergerkiller.generated.net.minecraft.world.level.WorldHandle;

import io.github.opencubicchunks.cubicchunks.api.world.ICubicWorld;
import io.github.opencubicchunks.cubicchunks.api.world.ICube;
import io.github.opencubicchunks.cubicchunks.api.world.ICubeProviderServer;

class Chunk {
#if version >= 1.18
    #require net.minecraft.world.level.chunk.Chunk public World getChunkWorld:getLevel();
    public (WorldHandle) World getWorld:getLevel();
#else
    #require net.minecraft.world.level.chunk.Chunk public World getChunkWorld:getWorld();
    public (WorldHandle) World getWorld();
#endif

#if version >= 1.14
    public int getLocX() {
        return instance.getPos().x;
    }

    public int getLocZ() {
        return instance.getPos().z;
    }
#else
    public int getLocX() {
        return instance.locX;
    }

    public int getLocZ() {
        return instance.locZ;
    }
#endif

    public java.util.List<Integer> getLoadedSectionCoordinates() {
#if exists io.github.opencubicchunks.cubicchunks.api.world.ICube
        World world = instance#getChunkWorld();
        if (world instanceof ICubicWorld && ((ICubicWorld) world).isCubicWorld()) {
            java.util.ArrayList result = new java.util.ArrayList();
            java.util.Iterator cubes_iter = instance.getLoadedCubes().iterator();
            while (cubes_iter.hasNext()) {
                result.add(Integer.valueOf(((ICube) cubes_iter.next()).getY()));
            }
            return result;
        }
#endif

        java.util.ArrayList result = new java.util.ArrayList();
        ChunkSection[] sections = instance.getSections();
        for (int i = 0; i < sections.length; i++) {
            ChunkSection section = sections[i];
            if (section != null) {
#if version >= 1.20
                result.add(Integer.valueOf(instance.getSectionYFromSectionIndex(i)));
#elseif version >= 1.18
                result.add(Integer.valueOf(section.bottomBlockY() >> 4));
#else
                result.add(Integer.valueOf(section.getYPosition() >> 4));
#endif
            }
        }
        return result;
    }

    // Converts ChunkSection into bkcl wrapper ChunkSection on <= 1.19.4 where ChunkSection had a field for reading y position
#if version <= 1.19.4
    #require net.minecraft.world.level.chunk.ChunkSection public static com.bergerkiller.bukkit.common.wrappers.ChunkSection wrapChunkSection(ChunkSection section) {
        ChunkSectionHandle sectionHandle = ChunkSectionHandle.createHandle(section);
  #if version >= 1.18
        int yPos = section.bottomBlockY();
  #else
        int yPos = section.getYPosition();
  #endif
        return new com.bergerkiller.bukkit.common.wrappers.ChunkSection(sectionHandle, yPos);
    }
#endif

    public com.bergerkiller.bukkit.common.wrappers.ChunkSection[] getSections() {
        ChunkSection[] sections = null;

#if exists io.github.opencubicchunks.cubicchunks.api.world.ICube
        World world = instance#getChunkWorld();
        if (world instanceof ICubicWorld && ((ICubicWorld) world).isCubicWorld()) {
            sections = new ChunkSection[16];
            for (int cy = 0; cy < 16; cy++) {
                sections[cy] = instance.getCube(cy).getStorage();
            }
        }
#endif

        if (sections == null) {
            sections = instance.getSections();
        }

        com.bergerkiller.bukkit.common.wrappers.ChunkSection[] wrapped;
        wrapped = new com.bergerkiller.bukkit.common.wrappers.ChunkSection[sections.length];

        for (int i = 0; i < sections.length; i++) {
            ChunkSection section = sections[i];
            if (section == null) {
                continue;
            }
#if version >= 1.20
            // Got to use the index of the array plus some other information to figure out the base y-position
            // ChunkSection no longer contains a field to check this
            int yPos = SectionPosition.sectionToBlockCoord(instance.getSectionYFromSectionIndex(i));
            ChunkSectionHandle sectionHandle = ChunkSectionHandle.createHandle(section);
            wrapped[i] = new com.bergerkiller.bukkit.common.wrappers.ChunkSection(sectionHandle, yPos);
#else
            wrapped[i] = #wrapChunkSection(section);
#endif
        }

        return wrapped;
    }

    #require net.minecraft.world.level.chunk.Chunk public ChunkSection getSectionRawImpl(int cy) {
#if exists io.github.opencubicchunks.cubicchunks.api.world.ICube
        World world = instance#getChunkWorld();
        if (world instanceof ICubicWorld && ((ICubicWorld) world).isCubicWorld()) {
            ICubeProviderServer provider = (ICubeProviderServer) ((ICubicWorld) world).getCubeCache();
            ICube cube = provider.getCube(instance.locX, cy, instance.locZ, ICubeProviderServer.Requirement.LOAD);
            if (cube != null) {
                return cube.getStorage();
            } else {
                return null;
            }
        }
#endif

        ChunkSection[] sections = instance.getSections();
#if version >= 1.17
        int index = instance.getSectionIndexFromSectionY(cy);
#else
        int index = cy;
#endif
        if (index >= 0 && index < sections.length) {
            return sections[index];
        } else {
            return null;
        }
    }

    public Object getSectionRaw(int cy) {
        return instance#getSectionRawImpl(cy);
    }

    public com.bergerkiller.bukkit.common.wrappers.ChunkSection getSection(int cy) {
        ChunkSection section = instance#getSectionRawImpl(cy);
        if (section == null) {
            return null;
        }

#if version >= 1.20
        // Got to use cy to compute the base block position, as ChunkSection lacks this information
        int yPos = SectionPosition.sectionToBlockCoord(cy);
        ChunkSectionHandle sectionHandle = ChunkSectionHandle.createHandle(section);
        return new com.bergerkiller.bukkit.common.wrappers.ChunkSection(sectionHandle, yPos);
#else
        return #wrapChunkSection(section);
#endif
    }

    public java.util.Collection<?> getRawTileEntities() {
#if version >= 1.17
        return instance.blockEntities.values();
#else
        return instance.tileEntities.values();
#endif
    }

    <code>
    public java.util.Collection<org.bukkit.block.BlockState> getTileEntities() {
        java.util.Collection<?> tileEntities = getRawTileEntities();
        com.bergerkiller.bukkit.common.conversion.blockstate.ChunkBlockStateConverter chunkBlockStateConverter;
        chunkBlockStateConverter = new com.bergerkiller.bukkit.common.conversion.blockstate.ChunkBlockStateConverter(getBukkitChunk());
        return new com.bergerkiller.mountiplex.conversion.util.ConvertingCollection(tileEntities, chunkBlockStateConverter);
    }
    </code>

    public (List<org.bukkit.entity.Entity>) List<Entity> getEntities() {
#if version >= 1.21 && exists ca.spottedleaf.moonrise.patches.chunk_system.level.entity.ChunkEntitySlices
        // New 1.21 Paper chunk system patches system
        WorldServer world = (WorldServer) instance#getChunkWorld();
        ca.spottedleaf.moonrise.patches.chunk_system.level.entity.EntityLookup entityLookup;
        entityLookup = world.moonrise$getEntityLookup();
        ca.spottedleaf.moonrise.patches.chunk_system.level.entity.ChunkEntitySlices slices;
        slices = entityLookup.getChunk(instance.getPos().x, instance.getPos().z);
        if (slices == null) {
            return java.util.Collections.emptyList();
        }

        #require ca.spottedleaf.moonrise.patches.chunk_system.level.entity.ChunkEntitySlices private final ca.spottedleaf.moonrise.common.list.EntityList entitySliceChunkEntities:entities;
        ca.spottedleaf.moonrise.common.list.EntityList entities = slices#entitySliceChunkEntities;

        // This replicates the code of getChunkEntities(), but sending it to a List of nms Entity instead of Bukkit entities
        final Entity[] entitiesArray = entities.getRawData();
        int size = Math.min(entitiesArray.length, entities.size());
        java.util.ArrayList result = new java.util.ArrayList(size);
        for (int i = 0; i < size; ++i) {
            Entity entity = entitiesArray[i];
            if (entity != null && entity.valid) {
                result.add(entity);
            }
        }
        return result;

#elseif version >= 1.17 && exists io.papermc.paper.world.ChunkEntitySlices public org.bukkit.entity.Entity[] getChunkEntities();
        // Paper API for storing entities inside chunk slices can be used
        WorldServer world = (WorldServer) instance#getChunkWorld();

    #if exists io.papermc.paper.chunk.system.entity.EntityLookup
        // New Paper Chunk System
        io.papermc.paper.chunk.system.entity.EntityLookup entityLookup = world.getEntityLookup();
        io.papermc.paper.world.ChunkEntitySlices slices = entityLookup.getChunk(instance.getPos().x, instance.getPos().z);
    #else
        #require net.minecraft.world.level.World protected final io.papermc.paper.world.EntitySliceManager entitySliceManager;
        io.papermc.paper.world.EntitySliceManager manager = world#entitySliceManager;
        io.papermc.paper.world.ChunkEntitySlices slices = manager.getChunk(instance.getPos().x, instance.getPos().z);
    #endif
        if (slices == null) {
            return java.util.Collections.emptyList();
        }

        #require io.papermc.paper.world.ChunkEntitySlices protected final com.destroystokyo.paper.util.maplist.EntityList entitySliceChunkEntities:entities;
        com.destroystokyo.paper.util.maplist.EntityList entities = slices#entitySliceChunkEntities;

        // This replicates the code of getChunkEntities(), but sending it to a List of nms Entity instead of Bukkit entities
        final Entity[] entitiesArray = entities.getRawData();
        int size = Math.min(entitiesArray.length, entities.size());
        java.util.ArrayList result = new java.util.ArrayList(size);
        for (int i = 0; i < size; ++i) {
            Entity entity = entitiesArray[i];
            if (entity != null && entity.valid) {
                result.add(entity);
            }
        }
        return result;

#elseif version >= 1.17
        // Down here is for Spigot / old Paper
        // Acquire the EntitySectionStorage instance of the world this Chunk is on
        #require net.minecraft.server.level.WorldServer private final net.minecraft.world.level.entity.PersistentEntitySectionManager entityManager;
        #require net.minecraft.world.level.entity.PersistentEntitySectionManager private final net.minecraft.world.level.entity.EntitySectionStorage sectionStorage;
        WorldServer world = (WorldServer) instance#getChunkWorld();
        net.minecraft.world.level.entity.PersistentEntitySectionManager entitySectionManager = world#entityManager;
        EntitySectionStorage storage = entitySectionManager#sectionStorage;

        // Acquire the sectionIds LongSet which allows for iterating a vertical slice of chunk sections
        #require net.minecraft.world.level.entity.EntitySectionStorage private final it.unimi.dsi.fastutil.longs.LongSortedSet sectionIds;
        it.unimi.dsi.fastutil.longs.LongSortedSet sectionIds = storage#sectionIds;

        // Acquire the sections LongObjectHashMap which stores the actual entity slices
        #require net.minecraft.world.level.entity.EntitySectionStorage private final it.unimi.dsi.fastutil.longs.Long2ObjectMap sections;
        it.unimi.dsi.fastutil.longs.Long2ObjectMap sections = storage#sections;

        // Iterate a subset of the sectionIds LongSet to find all vertical slices of this chunk storing entities
  #if version >= 1.18
        long sectionIdsStart = SectionPosition.asLong(instance.getPos().x, 0, instance.getPos().z);
        long sectionIdsEnd = SectionPosition.asLong(instance.getPos().x, -1, instance.getPos().z) + 1;
  #else
        long sectionIdsStart = SectionPosition.b(instance.getPos().x, 0, instance.getPos().z);
        long sectionIdsEnd = SectionPosition.b(instance.getPos().x, -1, instance.getPos().z) + 1;
  #endif

        it.unimi.dsi.fastutil.longs.LongBidirectionalIterator iterator = sectionIds.subSet(sectionIdsStart, sectionIdsEnd).iterator();
        List sliceLists = new java.util.ArrayList();
        while (iterator.hasNext()) {
            long key = iterator.nextLong();
            EntitySection section = (EntitySection) sections.get(key);
  #if version >= 1.18
            if (section != null && !section.isEmpty()) { // check not null or empty
  #else
            if (section != null && !section.a()) { // check not null or empty
  #endif
                // Retrieve the underlying storage
                #require net.minecraft.world.level.entity.EntitySection private final net.minecraft.util.EntitySlice storage;
                net.minecraft.util.EntitySlice sectionSlice = section#storage;
                sliceLists.add(com.bergerkiller.bukkit.common.conversion.type.HandleConversion.cbEntitySliceToList(sectionSlice));
            }
        }

        if (sliceLists.isEmpty()) {
            return java.util.Collections.emptyList();
        } else {
            return new List2D(sliceLists);
        }

#elseif exists net.minecraft.world.level.chunk.Chunk public final net.minecraft.util.EntitySlice<net.minecraft.world.entity.Entity>[] entitySlices;
        net.minecraft.util.EntitySlice[] slices = instance.entitySlices;
        int count = slices.length;
        List[] lists = new List[count];
        for (int i = 0; i < count; i++) {
            lists[i] = com.bergerkiller.bukkit.common.conversion.type.HandleConversion.cbEntitySliceToList(slices[i]);
        }
        return new List2D(lists);
#else
        return new List2D(instance.entitySlices);
#endif
    }

    public org.bukkit.Chunk getBukkitChunk() {
#if version >= 1.19.4 && !exists net.minecraft.world.level.chunk.Chunk public org.bukkit.Chunk bukkitChunk;
        return new org.bukkit.craftbukkit.CraftChunk(instance);
#else
        return instance.bukkitChunk;
#endif
    }

#if version >= 1.18
    public (BlockData) IBlockData getBlockData:getBlockState((IntVector3) BlockPosition blockposition);
#elseif version >= 1.13
    public (BlockData) IBlockData getBlockData:getType((IntVector3) BlockPosition blockposition);
#else
    public (BlockData) IBlockData getBlockData((IntVector3) BlockPosition blockposition);
#endif

#if version >= 1.18
    public (BlockData) IBlockData getBlockDataAtCoord(int x, int y, int z) {
        return instance.getBlockState(new BlockPosition(x, y, z));
    }
#elseif version >= 1.16
    public (BlockData) IBlockData getBlockDataAtCoord(int x, int y, int z) {
        return instance.getType(new BlockPosition(x, y, z));
    }
#elseif version >= 1.14
    public (BlockData) IBlockData getBlockDataAtCoord(int x, int y, int z) {
        BlockPosition$PooledBlockPosition blockPos = BlockPosition$PooledBlockPosition.f(x, y, z);
        try {
            return instance.getType((BlockPosition) blockPos);
        } finally {
            blockPos.close();
        }
    }
#elseif version >= 1.13
    public (BlockData) IBlockData getBlockDataAtCoord:getBlockData(int x, int y, int z);
#elseif version >= 1.9
    public (BlockData) IBlockData getBlockDataAtCoord:a(int x, int y, int z);
#else
    // Fallback on 1.8.8: generated code
    public (BlockData) IBlockData getBlockDataAtCoord(int x, int y, int z) {
        return instance.getBlockData(new BlockPosition(x, y, z));
    }
#endif

    // Since MC 1.13 updateFlags are used (See World.java setTypeAndData)
    public (BlockData) IBlockData setBlockData((IntVector3) BlockPosition blockposition, (BlockData) IBlockData iblockdata, int updateFlags) {
#if version >= 1.21.5
        return instance.setBlockState(blockposition, iblockdata, updateFlags);
#elseif version >= 1.18
        return instance.setBlockState(blockposition, iblockdata, (updateFlags & 64) != 0);
#elseif version >= 1.13.2
        return instance.setType(blockposition, iblockdata, (updateFlags & 64) != 0);
#elseif version >= 1.13
        return instance.a(blockposition, iblockdata, (updateFlags & 64) != 0);
#else
        return instance.a(blockposition, iblockdata);
#endif
    }

#if version >= 1.18
    // Note: Deprecated, does nothing it seems
    public void addEntity((EntityHandle) Entity entity);
#else
    public void addEntity:a((EntityHandle) Entity entity);
#endif

#if version >= 1.14
    public (com.bergerkiller.bukkit.common.wrappers.HeightMap) HeightMap getLightHeightMap(boolean initialize) {
        // Note: must always initialize, since the server does not store this data
        HeightMap heightMap = new HeightMap(instance, HeightMap$Type.values()[0]);
        com.bergerkiller.generated.net.minecraft.world.level.levelgen.HeightMapHandle.T.initialize.invoke(heightMap);
        return heightMap;
    }
#elseif version >= 1.13
    public (com.bergerkiller.bukkit.common.wrappers.HeightMap) HeightMap getLightHeightMap(boolean initialize) {
        HeightMap heightMap = instance.b(HeightMap$Type.LIGHT_BLOCKING);
        if (initialize) {
            heightMap.a();
        }
        return heightMap;
    }
#else
    public (com.bergerkiller.bukkit.common.wrappers.HeightMap) HeightMap getLightHeightMap(boolean initialize) {
        HeightMap heightMap = new com.bergerkiller.bukkit.common.internal.proxy.HeightMapProxy_1_12_2(instance, instance.heightMap);
        if (initialize) {
             com.bergerkiller.generated.net.minecraft.world.level.levelgen.HeightMapHandle.T.initialize.invoke(heightMap);
        }
        return heightMap;
    }
#endif

#if version >= 1.14
    public int getBrightness((EnumSkyBlockHandle) EnumSkyBlock enumskyblock, (IntVector3) BlockPosition position) {
        World world = instance#getChunkWorld();
        return world.getBrightness(enumskyblock, position);
    }
#else
    public int getBrightness((EnumSkyBlockHandle) EnumSkyBlock enumskyblock, (IntVector3) BlockPosition position);
#endif

#if version >= 1.18
    public int getTopSliceY:getHighestSectionPosition();
#elseif version >= 1.13
    public int getTopSliceY:b();
#else
    public int getTopSliceY:g();
#endif

    // MC 1.13: byte[] -> BiomeBase[] -> (1.16.2) BiomeSettingsMobs[]
    // public byte[] getBiomeIndex();

#if version >= 1.18
    public void addEntities:runPostLoad();
#else
    public void addEntities();
#endif

#if version >= 1.18
    public boolean checkCanSave:c(boolean isNotAutosave) {
        return instance.isUnsaved();
    }
#elseif version >= 1.14
    public boolean checkCanSave:c(boolean isNotAutosave) {
        return instance.isNeedsSaving();
    }
#elseif version >= 1.13
    public boolean checkCanSave:c(boolean isNotAutosave);
#else
    public boolean checkCanSave:a(boolean isNotAutosave);
#endif

#if version >= 1.21.2
    public void markDirty:markUnsaved();
#elseif version >= 1.18
    public void markDirty() {
        instance.setUnsaved(true);
    }
#elseif version >= 1.12
    public void markDirty();
#else
    public void markDirty:e();
#endif

    // this.someField = true in a(Entity) when entity is added
    // There is an accessor method which does the same, call that
    // Since 1.17 entities aren't stored in the Chunk instance anymore,
    // is the field still required at all?
    public void markEntitiesDirty() {
#select version >=
#case 1.21.2: instance.markUnsaved(); // Not entirely correct, do we need this?
#case 1.18:   instance.setUnsaved(true); // Not entirely correct, do we need this?
#case 1.17:   instance.markDirty(); // Not entirely correct, do we need this?
#case 1.14:   instance.d(true);
#case 1.13:   instance.f(true);
#case else:   instance.g(true);
#endselect
    }

    <code>
    public static ChunkHandle fromBukkit(org.bukkit.Chunk chunk) {
        if (chunk != null) {
            return createHandle(com.bergerkiller.bukkit.common.conversion.type.HandleConversion.toChunkHandle(chunk));
        } else {
            return null;
        }
    }
    </code>
}
