package net.minecraft.world.level;

import net.minecraft.world.level.ClipContext;

import net.minecraft.core.BlockPos;
import net.minecraft.resources.ResourceKey;
import net.minecraft.world.entity.Entity;
import net.minecraft.world.level.block.Block;
import net.minecraft.world.level.block.entity.BlockEntity;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.chunk.ChunkSource;
import net.minecraft.world.phys.AABB;
import net.minecraft.world.phys.HitResult;
import net.minecraft.world.phys.HitResult$Type;
import net.minecraft.world.phys.Vec3;
import net.minecraft.util.Mth;

import com.bergerkiller.bukkit.common.wrappers.BlockData;
import com.bergerkiller.bukkit.common.wrappers.BlockDataRegistry;
import com.bergerkiller.bukkit.common.wrappers.PlayerRespawnPoint;
import com.bergerkiller.bukkit.common.resources.DimensionType;
import com.bergerkiller.bukkit.common.bases.IntVector3;

import com.bergerkiller.generated.net.minecraft.world.entity.EntityHandle;
import com.bergerkiller.generated.net.minecraft.world.level.block.entity.BlockEntityHandle;
import com.bergerkiller.generated.net.minecraft.world.phys.AABBHandle;
import com.bergerkiller.generated.net.minecraft.world.phys.HitResultHandle;
import com.bergerkiller.generated.net.minecraft.world.phys.shapes.VoxelShapeHandle;
import com.bergerkiller.generated.net.minecraft.util.RandomSourceHandle;

class Level extends BlockGetter {
    public final (RandomSourceHandle) net.minecraft.util.RandomSource random;
    protected optional (Object) ChunkSource field_chunkProvider:chunkProvider;
    private final (org.bukkit.World) org.bukkit.craftbukkit.CraftWorld bukkitWorld:world;

    //TODO: Yeet this API its bad
    public void setKeepSpawnInMemoryDuringInit(boolean loaded) {
#if version >= 1.20.5
        // Defer to Bukkit API as it uses gamerules now
        instance.getWorld().setKeepSpawnInMemory(loaded);
#else
        #require net.minecraft.world.level.Level public boolean keepSpawnInMemory;
        instance#keepSpawnInMemory = loaded;
#endif
    }

    public void method_profiler_begin(String label) {
#if version >= 1.21.2
        net.minecraft.util.profiling.ProfilerFiller profiler = net.minecraft.util.profiling.Profiler.get();
        profiler.push(label);
#elseif version >= 1.19
        net.minecraft.util.profiling.ProfilerFiller profiler = instance.getProfiler();
        if (profiler != null) {
            profiler.push(label);
        }
#elseif version >= 1.18
        instance.getProfiler().push(label);
#elseif version >= 1.14
        instance.getMethodProfiler().enter(label);
#elseif exists net.minecraft.util.profiling.ActiveProfiler public void enter(String label)
        instance.methodProfiler.enter(label);
#else
        instance.methodProfiler.a(label);
#endif
    }

    public void method_profiler_end() {
#if version >= 1.21.2
        net.minecraft.util.profiling.ProfilerFiller profiler = net.minecraft.util.profiling.Profiler.get();
        profiler.pop();
#elseif version >= 1.19
        net.minecraft.util.profiling.ProfilerFiller profiler = instance.getProfiler();
        if (profiler != null) {
            profiler.pop();
        }
#elseif version >= 1.18
        instance.getProfiler().pop();
#elseif version >= 1.14
        instance.getMethodProfiler().exit();
#elseif exists net.minecraft.util.profiling.ActiveProfiler public void exit()
        instance.methodProfiler.exit();
#elseif version >= 1.13
        instance.methodProfiler.e();
#else
        instance.methodProfiler.b();
#endif
    }

    public (org.bukkit.World) org.bukkit.craftbukkit.CraftWorld getWorld();

#if exists net.minecraft.world.level.Level public org.bukkit.craftbukkit.CraftServer getCraftServer();
    public (org.bukkit.Server) org.bukkit.craftbukkit.CraftServer getServer:getCraftServer();
#else
    public (org.bukkit.Server) org.bukkit.craftbukkit.CraftServer getServer();
#endif

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

    /* This is runtime generated to improve performance, avoiding the cost of creating BlockPos objects */
    public BlockData getBlockDataAtCoord(int x, int y, int z) {
#if exists net.minecraft.world.level.Level public boolean captureTreeGeneration; && !exists io.github.opencubicchunks.cubicchunks.api.world.ICube
        // CraftBukkit does some special stuff, sometimes, when captureTreeGeneration is set
        if (instance.captureTreeGeneration) {
  #if version >= 1.18
            BlockState blockData = instance.getBlockState(new BlockPos(x, y, z));
  #else
            BlockState blockData = instance.getType(new BlockPos(x, y, z));
  #endif
            return BlockDataRegistry.fromBlockData(blockData);
        }

        // Get chunk of block and it's sections
  #if version >= 1.18
        net.minecraft.world.level.chunk.LevelChunk chunk = (net.minecraft.world.level.chunk.LevelChunk) instance.getChunk(x >> 4, z >> 4);
  #else
        net.minecraft.world.level.chunk.LevelChunk chunk = (net.minecraft.world.level.chunk.LevelChunk) instance.getChunkAt(x >> 4, z >> 4);
  #endif
        net.minecraft.world.level.chunk.LevelChunkSection[] sections = chunk.getSections();

        // Compute the section slice from the y-coordinate
  #if version >= 1.17
        int sectionIndex = chunk.getSectionIndex(y);
  #else
        int sectionIndex = y >> 4;
  #endif
        if (sectionIndex >= 0 && sectionIndex < sections.length) {
            net.minecraft.world.level.chunk.LevelChunkSection section = sections[sectionIndex];
            if (section != null) {
                // Note: we don't use getType/getBlockState() to eliminate a cast
  #if version >= 1.18
                Object blockData = section.getStates().get(x & 0xF, y & 0xF, z & 0xF);
  #elseif version >= 1.9
                Object blockData = section.getBlocks().a(x & 0xF, y & 0xF, z & 0xF);
  #else
                Object blockData = section.getType(x & 0xF, y & 0xF, z & 0xF);
  #endif
                return BlockDataRegistry.fromBlockData(blockData);
            }
        }

        // Fallback is AIR
        return com.bergerkiller.bukkit.common.wrappers.BlockData.AIR;
#elseif version >= 1.18
        // Weird stuff on forge
        BlockState blockData = instance.getBlockState(new BlockPos(x, y, z));
        return BlockDataRegistry.fromBlockData(blockData);
#else
        // Weird stuff on forge
        BlockState blockData = instance.getType(new BlockPos(x, y, z));
        return BlockDataRegistry.fromBlockData(blockData);
#endif
    }

    <code>
    public static final int UPDATE_PHYSICS = 0x1; // flag specifying block physics should occur after the change
    public static final int UPDATE_NOTIFY = 0x2; // flag specifying the change should be updated to players
    public static final int UPDATE_DEFAULT = (UPDATE_PHYSICS | UPDATE_NOTIFY); // default flags used when updating block types
    </code>

#if version >= 1.18
    public optional (Object) ChunkSource getChunkProvider:getChunkSource();
    public boolean setBlockData:setBlock((IntVector3) BlockPos blockposition, (BlockData) BlockState iblockdata, int updateFlags);
    public long getTime:getGameTime();
#else
    public optional (Object) ChunkSource getChunkProvider();
    public boolean setBlockData:setTypeAndData((IntVector3) BlockPos blockposition, (BlockData) BlockState iblockdata, int updateFlags);
    public long getTime();
#endif

#if version >= 1.18
    public (DimensionType) net.minecraft.world.level.dimension.DimensionType getDimensionType:dimensionType();
#elseif version >= 1.16
    public (DimensionType) net.minecraft.world.level.dimension.DimensionType getDimensionType:getDimensionManager();
#else
    public (DimensionType) net.minecraft.world.level.dimension.DimensionType getDimensionType() {
  #if version >= 1.14.1
        // Since 1.14.1 Bukkit added a method to easily obtain the dimension type
        return instance.worldProvider.getDimensionManager().getType();

  #elseif version >= 1.14
        // Since 1.14 the DimensionType has multiple instances for many worlds
        // As a result, it is not an accurate way to obtain the world type
        // There is no getType() we can use yet, so the next best way is to check the type of world provider
        if (instance.worldProvider instanceof WorldProviderHell) {
            return net.minecraft.world.level.dimension.DimensionType.NETHER;
        } else if (instance.worldProvider instanceof WorldProviderTheEnd) {
            return net.minecraft.world.level.dimension.DimensionType.THE_END;
        } else {
            return net.minecraft.world.level.dimension.DimensionType.OVERWORLD;
        }
  #elseif version >= 1.9
        // Uses DimensionType. Since 1.13.1 DimensionType is no longer an enum.
        // However, WorldProvider does not return the DimensionType of the world, but
        // rather the enum constants, so the result of WorldProvider:getDimensionManager()
        // is safe to use!
        return instance.worldProvider.getDimensionManager();
  #else
        // Uses a dimension ID, instead of a DimensionType instance.
        // WorldProvider:getDimension() only returns -1, 0 or 1, as set in the constructor
        return net.minecraft.world.level.dimension.DimensionType.a(instance.worldProvider.getDimension());
  #endif
    }
#endif

#if version >= 1.18
    public boolean isWithinWorldBorder((EntityHandle) Entity entity) {
        return instance.getWorldBorder().isWithinBounds(entity.getBoundingBox());
    }
#elseif version >= 1.14
    public boolean isWithinWorldBorder((EntityHandle) Entity entity) {
        return instance.getWorldBorder().a(entity.getBoundingBox());
    }
#elseif version >= 1.13
    public boolean isWithinWorldBorder:i((EntityHandle) Entity entity);
#elseif version >= 1.11.2
    public boolean isWithinWorldBorder:g((EntityHandle) Entity entity);
#elseif version >= 1.8.3
    public boolean isWithinWorldBorder((EntityHandle) Entity entity) {
        return instance.a(instance.getWorldBorder(), entity);
    }
#else
    public boolean isWithinWorldBorder((EntityHandle) Entity entity) {
        return instance.a(instance.af(), entity);
    }
#endif

#if version >= 1.13
    private optional boolean getBlockCollisions:###((EntityHandle) Entity entity, (AABBHandle) AABB bounds, boolean flag, (List<AABBHandle>) List<AABB> list);
#elseif version >= 1.11.2
    private optional boolean getBlockCollisions:a((EntityHandle) Entity entity, (AABBHandle) AABB bounds, boolean flag, (List<AABBHandle>) List<AABB> list);
#else
    private optional boolean getBlockCollisions:###((EntityHandle) Entity entity, (AABBHandle) AABB bounds, boolean flag, (List<AABBHandle>) List<AABB> list);
#endif

    //#if version >= 1.13.1
    //    public (java.util.stream.Stream<VoxelShapeHandle>) java.util.stream.Stream<VoxelShape> getCollisionShapes:a((EntityHandle) Entity entity, (AABBHandle) AABB boundingBox, double dx, double dy, double dz);
    //#elseif version >= 1.13
    //    public (java.util.stream.Stream<VoxelShapeHandle>) VoxelShape getCollisionShapes:a((EntityHandle) Entity entity, (AABBHandle) AABB boundingBox, double dx, double dy, double dz);
    //#elseif version >= 1.11
    //    public (java.util.stream.Stream<VoxelShapeHandle>) List<AABB> getCollisionShapes((EntityHandle) Entity entity, (AABBHandle) AABB boundingBox, double dx, double dy, double dz) {
    //        return instance.getCubes(entity, boundingBox.b(dx, dy, dz));
    //    }
    //#else
    //    public (java.util.stream.Stream<VoxelShapeHandle>) List<AABB> getCollisionShapes((EntityHandle) Entity entity, (AABBHandle) AABB boundingBox, double dx, double dy, double dz) {
    //        return instance.getCubes(entity, boundingBox.a(dx, dy, dz));
    //    }
    //#endif
    //

#if version >= 1.18
    public boolean isNotCollidingWithBlocks:noCollision((EntityHandle) Entity entity, (AABBHandle) AABB axisalignedbb);
#elseif version >= 1.13
    public boolean isNotCollidingWithBlocks:getCubes((EntityHandle) Entity entity, (AABBHandle) AABB axisalignedbb);
#else
    public boolean isNotCollidingWithBlocks((EntityHandle) Entity entity, (AABBHandle) AABB axisalignedbb) {
        return instance.getCubes(entity, axisalignedbb).isEmpty();
    }
#endif

    // Used by the EntityMoveHandler_1_8 only
#if version >= 1.13
    public optional (List<AABBHandle>) List<AABB> opt_getCubes_1_8:###((EntityHandle) Entity entity, (AABBHandle) AABB axisalignedbb);
#else
    public optional (List<AABBHandle>) List<AABB> opt_getCubes_1_8:getCubes((EntityHandle) Entity entity, (AABBHandle) AABB axisalignedbb);
#endif

#if version >= 1.18
    public (List<?>) List<T extends Entity> getRawEntitiesOfType:getEntitiesOfClass((Class<?>) Class<T extends Entity> rawType, (AABBHandle) AABB bounds);
#elseif version >= 1.17
    public (List<?>) List<T extends Entity> getRawEntitiesOfType:a((Class<?>) Class<T extends Entity> rawType, (AABBHandle) AABB bounds);
#else
    public (List<?>) List<T extends Entity> getRawEntitiesOfType:a((Class<?>) Class<? extends Entity> rawType, (AABBHandle) AABB bounds);
#endif

    public (List<EntityHandle>) List<Entity> getNearbyEntities:getEntities((EntityHandle) Entity entity, (AABBHandle) AABB axisalignedbb);

#if version >= 1.18
    public (BlockEntityHandle) BlockEntity getTileEntity:getBlockEntity((IntVector3) BlockPos blockposition);
#else
    public (BlockEntityHandle) BlockEntity getTileEntity((IntVector3) BlockPos blockposition);
#endif

#if version >= 1.18
    public boolean isBurnArea((AABBHandle) AABB bounds) {
        int b_minX = Mth.floor(bounds.minX);
        int b_minY = Mth.floor(bounds.minY);
        int b_minZ = Mth.floor(bounds.minZ);
        int b_maxX = Mth.floor(bounds.maxX);
        int b_maxY = Mth.floor(bounds.maxY);
        int b_maxZ = Mth.floor(bounds.maxZ);
        if (!instance.hasChunksAt(b_minX, b_minY, b_minZ, b_maxX, b_maxY, b_maxZ)) {
            return false;
        }

        java.util.Iterator blockPosIter = BlockPos.betweenClosed(b_minX, b_minY, b_minZ, b_maxX, b_maxY, b_maxZ).iterator();
        while (blockPosIter.hasNext()) {
            BlockPos pos = (BlockPos) blockPosIter.next();
            BlockState iblockdata = instance.getBlockState(pos);
  #if version >= 1.18.2
            if (iblockdata.is((net.minecraft.tags.TagKey) net.minecraft.tags.BlockTags.FIRE) || iblockdata.is(net.minecraft.world.level.block.Blocks.LAVA)) {
  #else
            if (iblockdata.is((net.minecraft.tags.Tag) net.minecraft.tags.BlockTags.FIRE) || iblockdata.is(net.minecraft.world.level.block.Blocks.LAVA)) {
  #endif
                return true;
            }
        }

        return false;
    }
#elseif version >= 1.16
    public boolean isBurnArea((AABBHandle) AABB bounds) {
        java.util.stream.Stream iblockdata_in_bounds_stream = instance.c(bounds);
        java.util.Iterator iter = iblockdata_in_bounds_stream.iterator();
        while (iter.hasNext()) {
            BlockState iblockdata1 = (BlockState) iter.next();
            if (iblockdata1.a((net.minecraft.tags.Tag) net.minecraft.tags.BlockTags.FIRE) || iblockdata1.a(net.minecraft.world.level.block.Blocks.LAVA)) {
                return true;
            }
        }
        return false;
    }
#elseif version >= 1.15
    public boolean isBurnArea:c((AABBHandle) AABB bounds);
#elseif version >= 1.13
    public boolean isBurnArea:b((AABBHandle) AABB bounds);
#elseif version >= 1.11.2
    public boolean isBurnArea:e((AABBHandle) AABB bounds);
#elseif version >= 1.9
    public boolean isBurnArea:f((AABBHandle) AABB bounds);
#else
    public boolean isBurnArea:e((AABBHandle) AABB bounds);
#endif

    // Note: main thread use only! Otherwise, use EntityAddRemoveHandler.
#if version >= 1.9
    public (org.bukkit.entity.Entity) Entity getEntityById:getEntity(int entityId);
#else
    public (org.bukkit.entity.Entity) Entity getEntityById:a(int entityId);
#endif

    // Gone since 1.14
    // public float getExplosionFactor:a((org.bukkit.util.Vector) Vec3 vec3d, (AABBHandle) AABB bounds);

#if version >= 1.18
    public boolean areChunksLoaded((IntVector3) BlockPos blockposition, int distance) {
        return instance.hasChunksAt(blockposition.getX() - distance, blockposition.getY() - distance, blockposition.getZ() - distance,
                                    blockposition.getX() + distance, blockposition.getY() + distance, blockposition.getZ() + distance);
    }
#elseif version >= 1.14
    // Gone since 1.14, but isAreaLoaded still exists
    public boolean areChunksLoaded((IntVector3) BlockPos blockposition, int distance) {
        return instance.isAreaLoaded(blockposition.getX() - distance, blockposition.getY() - distance, blockposition.getZ() - distance,
                                     blockposition.getX() + distance, blockposition.getY() + distance, blockposition.getZ() + distance);
    }
#else
    // Note: moved to IWorldReader interface on MC 1.13
    public boolean areChunksLoaded((IntVector3) BlockPos blockposition, int distance);
#endif

#if version >= 1.14
    // Fallback...
    public (HitResultHandle) HitResult rayTrace((org.bukkit.util.Vector) Vec3 point1, (org.bukkit.util.Vector) Vec3 point2) {
        ClipContext rayTrace = new ClipContext(point1, point2, ClipContext$Block.OUTLINE, ClipContext$Fluid.NONE, null);
  #if version >= 1.18
        HitResult result = instance.clip(rayTrace);
  #else
        HitResult result = instance.rayTrace(rayTrace);
  #endif

        if (result != null && result.getType() == HitResult$Type.MISS) {
            result = null;
        }
        return result;
    }
#elseif version >= 1.13
    // Fallback...
    public (HitResultHandle) HitResult rayTrace((org.bukkit.util.Vector) Vec3 point1, (org.bukkit.util.Vector) Vec3 point2) {
        return instance.rayTrace(point1, point2);
    }
#else
    public (HitResultHandle) HitResult rayTrace((org.bukkit.util.Vector) Vec3 point1, (org.bukkit.util.Vector) Vec3 point2);
#endif

    <code>
    public void applyBlockPhysics(IntVector3 position, BlockData causeType, boolean self) {
        applyBlockPhysicsAround(position, causeType);
        if (self) {
            applyBlockPhysics(position, causeType);
        }
    }
    </code>

#if version >= 1.11 && version <= 1.12.2
    // Shortly had a boolean flag between MC 1.11 and 1.12.2
    public void applyBlockPhysicsAround((IntVector3) BlockPos position, (BlockData) Block causeType) {
        instance.applyPhysics(position, causeType, false);
    }
#elseif version >= 1.18
    public void applyBlockPhysicsAround:updateNeighborsAt((IntVector3) BlockPos position, (BlockData) Block causeType);
#else
    public void applyBlockPhysicsAround:applyPhysics((IntVector3) BlockPos position, (BlockData) Block causeType);
#endif

    public void applyBlockPhysics((IntVector3) BlockPos position, (BlockData) Block causeType) {
#if version >= 1.21.2
        instance.neighborChanged(position, causeType, (net.minecraft.world.level.redstone.Orientation) null);
#elseif version >= 1.18
        instance.neighborChanged(position, causeType, position);
#elseif version >= 1.11
        instance.a(position, causeType, position);
#elseif version >= 1.9
        instance.e(position, causeType);
#else
        instance.d(position, causeType);
#endif
    }

#if version >= 1.21.2
    public int getMinBuildHeight:getMinY();
    public int getMaxBuildHeight() {
        return instance.getMaxY() + 1;
    }
#elseif version >= 1.17
    public int getMinBuildHeight();
    public int getMaxBuildHeight();
#else
    // Note: while getMaxBuildHeight exists pre-1.17, it is only honored
    //       when players place down blocks. Blocks still exist beyond the
    //       max build height. For this reason we're effectively ignoring
    //       it.
    public int getMinBuildHeight() {
        return 0;
    }
    public int getMaxBuildHeight() {
        return 256;
    }
#endif

    public int getNetherPortalSearchRadius() {
        // Paper per-world configuration
#if exists com.destroystokyo.paper.PaperWorldConfig public int portalSearchRadius;
  #if exists net.minecraft.world.level.Level public final com.destroystokyo.paper.PaperWorldConfig paperSpigotConfig;
        return instance.paperSpigotConfig.portalSearchRadius;
  #else
        return instance.paperConfig.portalSearchRadius;
  #endif
#endif

        return 128; // Vanilla default
    }

    public int getNetherPortalCreateRadius() {
        // Paper per-world configuration
#if exists com.destroystokyo.paper.PaperWorldConfig public int portalCreateRadius;
  #if exists net.minecraft.world.level.Level public final com.destroystokyo.paper.PaperWorldConfig paperSpigotConfig;
        return instance.paperSpigotConfig.portalCreateRadius;
  #else
        return instance.paperConfig.portalCreateRadius;
  #endif
#endif

        return 16; // Vanilla default
    }

    <code>
    public org.bukkit.World toBukkit() {
        return com.bergerkiller.bukkit.common.conversion.Conversion.toWorld.convert(getRaw());
    }

    public static LevelHandle fromBukkit(org.bukkit.World world) {
        return createHandle(com.bergerkiller.bukkit.common.conversion.Conversion.toWorldHandle.convert(world));
    }
    </code>
}

// Since MC 1.13.1
optional class ForcedChunksSavedData {
}

interface BlockGetter {
}
