package net.minecraft.world.level;

import net.minecraft.core.BlockPosition;
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.TileEntity;
import net.minecraft.world.level.block.state.IBlockData;
import net.minecraft.world.level.chunk.IChunkProvider;
import net.minecraft.world.level.dimension.DimensionManager;
import net.minecraft.world.phys.AxisAlignedBB;
import net.minecraft.world.phys.MovingObjectPosition;
import net.minecraft.world.phys.MovingObjectPosition$EnumMovingObjectType;
import net.minecraft.world.phys.Vec3D;
import net.minecraft.util.MathHelper;

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.TileEntityHandle;
import com.bergerkiller.generated.net.minecraft.world.phys.AxisAlignedBBHandle;
import com.bergerkiller.generated.net.minecraft.world.phys.MovingObjectPositionHandle;
import com.bergerkiller.generated.net.minecraft.world.phys.shapes.VoxelShapeHandle;
import com.bergerkiller.generated.net.minecraft.util.RandomSourceHandle;

class World extends IBlockAccess {
    public final (RandomSourceHandle) net.minecraft.util.RandomSource random;
    protected optional (Object) IChunkProvider 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.World public boolean keepSpawnInMemory;
        instance#keepSpawnInMemory = loaded;
#endif
    }

    public void method_profiler_begin(String label) {
#if version >= 1.21.2
        net.minecraft.util.profiling.GameProfilerFiller profiler = net.minecraft.util.profiling.Profiler.get();
        profiler.push(label);
#elseif version >= 1.19
        net.minecraft.util.profiling.GameProfilerFiller 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.MethodProfiler 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.GameProfilerFiller profiler = net.minecraft.util.profiling.Profiler.get();
        profiler.pop();
#elseif version >= 1.19
        net.minecraft.util.profiling.GameProfilerFiller 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.MethodProfiler 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.World 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) IBlockData getBlockData:getBlockState((IntVector3) BlockPosition blockposition);
#elseif forge_nms_obfuscated
    public (BlockData) IBlockData getBlockData:o((IntVector3) BlockPosition blockposition);
#else
    public (BlockData) IBlockData getBlockData:getType((IntVector3) BlockPosition blockposition);
#endif

    /* This is runtime generated to improve performance, avoiding the cost of creating BlockPosition objects */
    public BlockData getBlockDataAtCoord(int x, int y, int z) {
#if exists net.minecraft.world.level.World 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
            IBlockData blockData = instance.getBlockState(new BlockPosition(x, y, z));
  #else
            IBlockData blockData = instance.getType(new BlockPosition(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.Chunk chunk = (net.minecraft.world.level.chunk.Chunk) instance.getChunk(x >> 4, z >> 4);
  #else
        net.minecraft.world.level.chunk.Chunk chunk = (net.minecraft.world.level.chunk.Chunk) instance.getChunkAt(x >> 4, z >> 4);
  #endif
        net.minecraft.world.level.chunk.ChunkSection[] 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.ChunkSection 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
        IBlockData blockData = instance.getBlockState(new BlockPosition(x, y, z));
        return BlockDataRegistry.fromBlockData(blockData);
#else
        // Weird stuff on forge
        IBlockData blockData = instance.getType(new BlockPosition(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) IChunkProvider getChunkProvider:getChunkSource();
    public boolean setBlockData:setBlock((IntVector3) BlockPosition blockposition, (BlockData) IBlockData iblockdata, int updateFlags);
    public long getTime:getGameTime();
#else
    public optional (Object) IChunkProvider getChunkProvider();
    public boolean setBlockData:setTypeAndData((IntVector3) BlockPosition blockposition, (BlockData) IBlockData iblockdata, int updateFlags);
    public long getTime();
#endif

#if version >= 1.18
    public (DimensionType) DimensionManager getDimensionType:dimensionType();
#elseif version >= 1.16
    public (DimensionType) DimensionManager getDimensionType:getDimensionManager();
#else
    public (DimensionType) DimensionManager 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 DimensionManager 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 DimensionManager.NETHER;
        } else if (instance.worldProvider instanceof WorldProviderTheEnd) {
            return DimensionManager.THE_END;
        } else {
            return DimensionManager.OVERWORLD;
        }
  #elseif version >= 1.9
        // Uses DimensionManager. Since 1.13.1 DimensionManager is no longer an enum.
        // However, WorldProvider does not return the DimensionManager 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 DimensionManager instance.
        // WorldProvider:getDimension() only returns -1, 0 or 1, as set in the constructor
        return DimensionManager.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, (AxisAlignedBBHandle) AxisAlignedBB bounds, boolean flag, (List<AxisAlignedBBHandle>) List<AxisAlignedBB> list);
#elseif version >= 1.11.2
    private optional boolean getBlockCollisions:a((EntityHandle) Entity entity, (AxisAlignedBBHandle) AxisAlignedBB bounds, boolean flag, (List<AxisAlignedBBHandle>) List<AxisAlignedBB> list);
#else
    private optional boolean getBlockCollisions:###((EntityHandle) Entity entity, (AxisAlignedBBHandle) AxisAlignedBB bounds, boolean flag, (List<AxisAlignedBBHandle>) List<AxisAlignedBB> list);
#endif

    //#if version >= 1.13.1
    //    public (java.util.stream.Stream<VoxelShapeHandle>) java.util.stream.Stream<VoxelShape> getCollisionShapes:a((EntityHandle) Entity entity, (AxisAlignedBBHandle) AxisAlignedBB boundingBox, double dx, double dy, double dz);
    //#elseif version >= 1.13
    //    public (java.util.stream.Stream<VoxelShapeHandle>) VoxelShape getCollisionShapes:a((EntityHandle) Entity entity, (AxisAlignedBBHandle) AxisAlignedBB boundingBox, double dx, double dy, double dz);
    //#elseif version >= 1.11
    //    public (java.util.stream.Stream<VoxelShapeHandle>) List<AxisAlignedBB> getCollisionShapes((EntityHandle) Entity entity, (AxisAlignedBBHandle) AxisAlignedBB boundingBox, double dx, double dy, double dz) {
    //        return instance.getCubes(entity, boundingBox.b(dx, dy, dz));
    //    }
    //#else
    //    public (java.util.stream.Stream<VoxelShapeHandle>) List<AxisAlignedBB> getCollisionShapes((EntityHandle) Entity entity, (AxisAlignedBBHandle) AxisAlignedBB 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, (AxisAlignedBBHandle) AxisAlignedBB axisalignedbb);
#elseif version >= 1.13
    public boolean isNotCollidingWithBlocks:getCubes((EntityHandle) Entity entity, (AxisAlignedBBHandle) AxisAlignedBB axisalignedbb);
#else
    public boolean isNotCollidingWithBlocks((EntityHandle) Entity entity, (AxisAlignedBBHandle) AxisAlignedBB axisalignedbb) {
        return instance.getCubes(entity, axisalignedbb).isEmpty();
    }
#endif

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

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

    public (List<EntityHandle>) List<Entity> getNearbyEntities:getEntities((EntityHandle) Entity entity, (AxisAlignedBBHandle) AxisAlignedBB axisalignedbb);

#if version >= 1.18
    public (TileEntityHandle) TileEntity getTileEntity:getBlockEntity((IntVector3) BlockPosition blockposition);
#else
    public (TileEntityHandle) TileEntity getTileEntity((IntVector3) BlockPosition blockposition);
#endif

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

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

        return false;
    }
#elseif version >= 1.16
    public boolean isBurnArea((AxisAlignedBBHandle) AxisAlignedBB bounds) {
        java.util.stream.Stream iblockdata_in_bounds_stream = instance.c(bounds);
        java.util.Iterator iter = iblockdata_in_bounds_stream.iterator();
        while (iter.hasNext()) {
            IBlockData iblockdata1 = (IBlockData) iter.next();
            if (iblockdata1.a((net.minecraft.tags.Tag) net.minecraft.tags.TagsBlock.FIRE) || iblockdata1.a(net.minecraft.world.level.block.Blocks.LAVA)) {
                return true;
            }
        }
        return false;
    }
#elseif version >= 1.15
    public boolean isBurnArea:c((AxisAlignedBBHandle) AxisAlignedBB bounds);
#elseif version >= 1.13
    public boolean isBurnArea:b((AxisAlignedBBHandle) AxisAlignedBB bounds);
#elseif version >= 1.11.2
    public boolean isBurnArea:e((AxisAlignedBBHandle) AxisAlignedBB bounds);
#elseif version >= 1.9
    public boolean isBurnArea:f((AxisAlignedBBHandle) AxisAlignedBB bounds);
#else
    public boolean isBurnArea:e((AxisAlignedBBHandle) AxisAlignedBB 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) Vec3D vec3d, (AxisAlignedBBHandle) AxisAlignedBB bounds);

#if version >= 1.18
    public boolean areChunksLoaded((IntVector3) BlockPosition 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) BlockPosition 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) BlockPosition blockposition, int distance);
#endif

#if version >= 1.14
    // Fallback...
    public (MovingObjectPositionHandle) MovingObjectPosition rayTrace((org.bukkit.util.Vector) Vec3D point1, (org.bukkit.util.Vector) Vec3D point2) {
        RayTrace rayTrace = new RayTrace(point1, point2, RayTrace$BlockCollisionOption.OUTLINE, RayTrace$FluidCollisionOption.NONE, null);
  #if version >= 1.18
        MovingObjectPosition result = instance.clip(rayTrace);
  #else
        MovingObjectPosition result = instance.rayTrace(rayTrace);
  #endif

        if (result != null && result.getType() == MovingObjectPosition$EnumMovingObjectType.MISS) {
            result = null;
        }
        return result;
    }
#elseif version >= 1.13
    // Fallback...
    public (MovingObjectPositionHandle) MovingObjectPosition rayTrace((org.bukkit.util.Vector) Vec3D point1, (org.bukkit.util.Vector) Vec3D point2) {
        return instance.rayTrace(point1, point2);
    }
#else
    public (MovingObjectPositionHandle) MovingObjectPosition rayTrace((org.bukkit.util.Vector) Vec3D point1, (org.bukkit.util.Vector) Vec3D 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) BlockPosition position, (BlockData) Block causeType) {
        instance.applyPhysics(position, causeType, false);
    }
#elseif version >= 1.18
    public void applyBlockPhysicsAround:updateNeighborsAt((IntVector3) BlockPosition position, (BlockData) Block causeType);
#else
    public void applyBlockPhysicsAround:applyPhysics((IntVector3) BlockPosition position, (BlockData) Block causeType);
#endif

    public void applyBlockPhysics((IntVector3) BlockPosition 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.World 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.World 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 WorldHandle fromBukkit(org.bukkit.World world) {
        return createHandle(com.bergerkiller.bukkit.common.conversion.Conversion.toWorldHandle.convert(world));
    }
    </code>
}

// Since MC 1.13.1
optional class ForcedChunk {
}

interface IBlockAccess {
}
