package net.minecraft.world.level.chunk;

import net.minecraft.core.BlockPos;
import net.minecraft.core.SectionPos;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.world.entity.Entity;
import net.minecraft.world.level.chunk.LevelChunkSection;
import net.minecraft.world.level.LightLayer;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.levelgen.Heightmap;
import net.minecraft.world.level.levelgen.Heightmap$Types;
import net.minecraft.world.level.Level;
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.LevelChunkHandle;
import com.bergerkiller.generated.net.minecraft.world.level.chunk.LevelChunkSectionHandle;
import com.bergerkiller.generated.net.minecraft.world.level.LightLayerHandle;

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

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 LevelChunk {
#if version >= 1.18
    #require net.minecraft.world.level.chunk.LevelChunk public Level getChunkWorld:getLevel();
    public (LevelHandle) Level getWorld:getLevel();
#else
    #require net.minecraft.world.level.chunk.LevelChunk public Level getChunkWorld:getWorld();
    public (LevelHandle) Level getWorld();
#endif

#if version >= 26.1
    public int getLocX() { return instance.getPos().x(); }
    public int getLocZ() { return instance.getPos().z(); }
#elseif 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
        Level 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();
        LevelChunkSection[] sections = instance.getSections();
        for (int i = 0; i < sections.length; i++) {
            LevelChunkSection 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 LevelChunkSection into bkcl wrapper ChunkSection on <= 1.19.4 where LevelChunkSection had a field for reading y position
#if version <= 1.19.4
    #require net.minecraft.world.level.chunk.LevelChunkSection public static com.bergerkiller.bukkit.common.wrappers.ChunkSection wrapChunkSection(LevelChunkSection section) {
        LevelChunkSectionHandle sectionHandle = LevelChunkSectionHandle.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() {
        LevelChunkSection[] sections = null;

#if exists io.github.opencubicchunks.cubicchunks.api.world.ICube
        Level world = instance#getChunkWorld();
        if (world instanceof ICubicWorld && ((ICubicWorld) world).isCubicWorld()) {
            sections = new LevelChunkSection[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++) {
            LevelChunkSection 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
            // LevelChunkSection no longer contains a field to check this
            int yPos = SectionPos.sectionToBlockCoord(instance.getSectionYFromSectionIndex(i));
            LevelChunkSectionHandle sectionHandle = LevelChunkSectionHandle.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.LevelChunk public LevelChunkSection getSectionRawImpl(int cy) {
#if exists io.github.opencubicchunks.cubicchunks.api.world.ICube
        Level 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

        LevelChunkSection[] 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) {
        LevelChunkSection 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 = SectionPos.sectionToBlockCoord(cy);
        LevelChunkSectionHandle sectionHandle = LevelChunkSectionHandle.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 >= 26.1
        int chunkX = instance.getPos().x();
        int chunkZ = instance.getPos().z();
  #elseif version >= 1.14
        int chunkX = instance.getPos().x;
        int chunkZ = instance.getPos().z;
  #else
        int chunkX = instance.locX;
        int chunkZ = instance.locZ;
  #endif

#if version >= 1.21 && exists ca.spottedleaf.moonrise.patches.chunk_system.level.entity.ChunkEntitySlices
        // New 1.21 Paper chunk system patches system
        ServerLevel world = (ServerLevel) 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(chunkX, chunkZ);
        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
        ServerLevel world = (ServerLevel) 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(chunkX, chunkZ);
    #else
        #require net.minecraft.world.level.Level protected final io.papermc.paper.world.EntitySliceManager entitySliceManager;
        io.papermc.paper.world.EntitySliceManager manager = world#entitySliceManager;
        io.papermc.paper.world.ChunkEntitySlices slices = manager.getChunk(chunkX, chunkZ);
    #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.ServerLevel 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;
        ServerLevel world = (ServerLevel) 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 = SectionPos.asLong(chunkX, 0, chunkZ);
        long sectionIdsEnd = SectionPos.asLong(chunkX, -1, chunkZ) + 1;
  #else
        long sectionIdsStart = SectionPos.b(chunkX, 0, chunkZ);
        long sectionIdsEnd = SectionPos.b(chunkX, -1, chunkZ) + 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.ClassInstanceMultiMap storage;
                net.minecraft.util.ClassInstanceMultiMap 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.LevelChunk public final net.minecraft.util.ClassInstanceMultiMap<net.minecraft.world.entity.Entity>[] entitySlices;
        net.minecraft.util.ClassInstanceMultiMap[] 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.LevelChunk public org.bukkit.Chunk bukkitChunk;
        return new org.bukkit.craftbukkit.CraftChunk(instance);
#else
        return instance.bukkitChunk;
#endif
    }

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

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

    // Since MC 1.13 updateFlags are used (See Level.java setTypeAndData)
    public (BlockData) BlockState setBlockData((IntVector3) BlockPos blockposition, (BlockData) BlockState 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$Types.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$Types.LIGHT_BLOCKING);
        if (initialize) {
            heightMap.a();
        }
        return heightMap;
    }
#else
    public (com.bergerkiller.bukkit.common.wrappers.HeightMap) Heightmap getLightHeightMap(boolean initialize) {
        Heightmap heightMap = new Heightmap(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((LightLayerHandle) LightLayer enumskyblock, (IntVector3) BlockPos position) {
        Level world = instance#getChunkWorld();
        return world.getBrightness(enumskyblock, position);
    }
#else
    public int getBrightness((LightLayerHandle) LightLayer enumskyblock, (IntVector3) BlockPos 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 LevelChunkHandle fromBukkit(org.bukkit.Chunk chunk) {
        if (chunk != null) {
            return createHandle(com.bergerkiller.bukkit.common.conversion.type.HandleConversion.toChunkHandle(chunk));
        } else {
            return null;
        }
    }
    </code>
}
