package net.minecraft.world.level.block.state;

import net.minecraft.core.BlockPosition;
import net.minecraft.core.EnumDirection;
import net.minecraft.world.level.block.Block;
import net.minecraft.world.level.block.BlockCarpet;
import net.minecraft.world.phys.AxisAlignedBB;
import net.minecraft.world.level.block.SoundEffectType;
import net.minecraft.world.level.block.state.properties.IBlockState;
import net.minecraft.world.level.IBlockAccess;

import net.minecraft.world.phys.shapes.VoxelShape;
import net.minecraft.world.phys.shapes.VoxelShapeCollision;

import com.google.common.collect.ImmutableMap;

import com.bergerkiller.bukkit.common.collections.BlockFaceSet;

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

import com.bergerkiller.generated.net.minecraft.world.level.block.BlockHandle;
import com.bergerkiller.generated.net.minecraft.world.level.IBlockAccessHandle;
import com.bergerkiller.generated.net.minecraft.world.level.block.SoundEffectTypeHandle;
import com.bergerkiller.generated.net.minecraft.world.level.block.state.IBlockDataHandle;
import com.bergerkiller.generated.net.minecraft.world.level.block.state.properties.IBlockStateHandle;
import com.bergerkiller.generated.net.minecraft.world.phys.AxisAlignedBBHandle;

interface IBlockData {
    public abstract (BlockHandle) Block getBlock();

    <code>
    public void logStates() {
        for (java.util.Map.Entry<IBlockStateHandle, Comparable<?>> entry : getStates().entrySet()) {
            com.bergerkiller.bukkit.common.Logging.LOGGER.info(entry.getKey() + " = " + entry.getValue());
        }
    }

    public IBlockStateHandle findState(String key) {
        for (IBlockStateHandle blockState : getStates().keySet()) {
            if (blockState.getKeyToken().equals(key)) {
                return blockState;
            }
        }
        return null;
    }

    public IBlockDataHandle set(String key, Object value) {
        return set(findState(key), value);
    }

    public <T> T get(String key, Class<T> type) {
        return get(findState(key), type);
    }

    public <T> T get(IBlockStateHandle state, Class<T> type) {
        return com.bergerkiller.bukkit.common.conversion.Conversion.convert(get(state), type, null);
    }
    </code>

    // World access require check
#if version >= 1.18
    #require net.minecraft.world.level.block.Block public boolean isWorldAccessRequired:hasDynamicShape();
#elseif version >= 1.16
    #require net.minecraft.world.level.block.Block public boolean isWorldAccessRequired:o();
#elseif version >= 1.15
    #require net.minecraft.world.level.block.Block public boolean isWorldAccessRequired:q();
#elseif version >= 1.14
    #require net.minecraft.world.level.block.Block public boolean isWorldAccessRequired:p();
#endif

    // Returns null when the opaque faces aren't cached
    public BlockFaceSet getCachedOpaqueFaces() {
        Block block = instance.getBlock();

#if version >= 1.14
        // This flag is set to true by default for all blocks
        // Blocks that pass light through at all times, have it set to false
        // Examples are: doors, trapdoors
  #if version >= 1.18
        if (!instance.canOcclude()) {
  #elseif version >= 1.16
        if (!instance.l()) {
  #else
        if (!instance.o()) {
  #endif
            return com.bergerkiller.bukkit.common.collections.BlockFaceSet.NONE;
        }

        // Some blocks must be overrided to allow light to pass through
        // There doesn't appear to be a property we can use to query this :(
        if (block instanceof BlockCarpet) {
            return com.bergerkiller.bukkit.common.collections.BlockFaceSet.NONE;
        }

        // Check world access isn't required (see: Block)
        boolean worldAccessRequired = block#isWorldAccessRequired();
        if (worldAccessRequired) {
            return null;
        }

        // Query all 6 faces and produce a mask
        int mask = 0;
        IBlockAccess emptyBlockAccess = net.minecraft.world.level.BlockAccessAir.INSTANCE;
  #if version >= 1.18
        if (instance.isFaceSturdy(emptyBlockAccess, BlockPosition.ZERO, EnumDirection.NORTH)) {
            mask |= com.bergerkiller.bukkit.common.collections.BlockFaceSet.MASK_NORTH;
        }
        if (instance.isFaceSturdy(emptyBlockAccess, BlockPosition.ZERO, EnumDirection.EAST)) {
            mask |= com.bergerkiller.bukkit.common.collections.BlockFaceSet.MASK_EAST;
        }
        if (instance.isFaceSturdy(emptyBlockAccess, BlockPosition.ZERO, EnumDirection.SOUTH)) {
            mask |= com.bergerkiller.bukkit.common.collections.BlockFaceSet.MASK_SOUTH;
        }
        if (instance.isFaceSturdy(emptyBlockAccess, BlockPosition.ZERO, EnumDirection.WEST)) {
            mask |= com.bergerkiller.bukkit.common.collections.BlockFaceSet.MASK_WEST;
        }
        if (instance.isFaceSturdy(emptyBlockAccess, BlockPosition.ZERO, EnumDirection.UP)) {
            mask |= com.bergerkiller.bukkit.common.collections.BlockFaceSet.MASK_UP;
        }
        if (instance.isFaceSturdy(emptyBlockAccess, BlockPosition.ZERO, EnumDirection.DOWN)) {
            mask |= com.bergerkiller.bukkit.common.collections.BlockFaceSet.MASK_DOWN;
        }
  #elseif version >= 1.14.4
        if (instance.d(emptyBlockAccess, BlockPosition.ZERO, EnumDirection.NORTH)) {
            mask |= com.bergerkiller.bukkit.common.collections.BlockFaceSet.MASK_NORTH;
        }
        if (instance.d(emptyBlockAccess, BlockPosition.ZERO, EnumDirection.EAST)) {
            mask |= com.bergerkiller.bukkit.common.collections.BlockFaceSet.MASK_EAST;
        }
        if (instance.d(emptyBlockAccess, BlockPosition.ZERO, EnumDirection.SOUTH)) {
            mask |= com.bergerkiller.bukkit.common.collections.BlockFaceSet.MASK_SOUTH;
        }
        if (instance.d(emptyBlockAccess, BlockPosition.ZERO, EnumDirection.WEST)) {
            mask |= com.bergerkiller.bukkit.common.collections.BlockFaceSet.MASK_WEST;
        }
        if (instance.d(emptyBlockAccess, BlockPosition.ZERO, EnumDirection.UP)) {
            mask |= com.bergerkiller.bukkit.common.collections.BlockFaceSet.MASK_UP;
        }
        if (instance.d(emptyBlockAccess, BlockPosition.ZERO, EnumDirection.DOWN)) {
            mask |= com.bergerkiller.bukkit.common.collections.BlockFaceSet.MASK_DOWN;
        }
  #else
        // Awkward! It's not cached yet. Hopefully this works, though.
        if (Block.d(instance, emptyBlockAccess, BlockPosition.ZERO, EnumDirection.NORTH)) {
            mask |= com.bergerkiller.bukkit.common.collections.BlockFaceSet.MASK_NORTH;
        }
        if (Block.d(instance, emptyBlockAccess, BlockPosition.ZERO, EnumDirection.EAST)) {
            mask |= com.bergerkiller.bukkit.common.collections.BlockFaceSet.MASK_EAST;
        }
        if (Block.d(instance, emptyBlockAccess, BlockPosition.ZERO, EnumDirection.SOUTH)) {
            mask |= com.bergerkiller.bukkit.common.collections.BlockFaceSet.MASK_SOUTH;
        }
        if (Block.d(instance, emptyBlockAccess, BlockPosition.ZERO, EnumDirection.WEST)) {
            mask |= com.bergerkiller.bukkit.common.collections.BlockFaceSet.MASK_WEST;
        }
        if (Block.d(instance, emptyBlockAccess, BlockPosition.ZERO, EnumDirection.UP)) {
            mask |= com.bergerkiller.bukkit.common.collections.BlockFaceSet.MASK_UP;
        }
        if (Block.d(instance, emptyBlockAccess, BlockPosition.ZERO, EnumDirection.DOWN)) {
            mask |= com.bergerkiller.bukkit.common.collections.BlockFaceSet.MASK_DOWN;
        }
  #endif

        // Bugfix: sometimes faces is 'all' while opacity < 15
        // When this happens, it would make transparent blocks block all light
        // We obviously don't want that!
        if (mask == com.bergerkiller.bukkit.common.collections.BlockFaceSet.ALL.mask()) {
  #if version >= 1.21.2
            int opacity = instance.getLightBlock();
  #elseif version >= 1.18
            int opacity = instance.getLightBlock(emptyBlockAccess, BlockPosition.ZERO);
  #else
            int opacity = instance.b(emptyBlockAccess, BlockPosition.ZERO);
  #endif
            if (opacity < 15) {
                mask = 0;
            }
        }

        return com.bergerkiller.bukkit.common.collections.BlockFaceSet.byMask(mask);

#elseif version >= 1.9
        return block.isOccluding(instance) ? com.bergerkiller.bukkit.common.collections.BlockFaceSet.ALL
                                           : com.bergerkiller.bukkit.common.collections.BlockFaceSet.NONE;
#else
        return block.isOccluding() ? com.bergerkiller.bukkit.common.collections.BlockFaceSet.ALL
                                   : com.bergerkiller.bukkit.common.collections.BlockFaceSet.NONE;
#endif
    }

    // Returns -1 if world access is required to figure it out
#if version >= 1.21.2
    public int getCachedOpacity() {
        // World access is never required - it is always cached
        // There isn't even a method to get the light level that makes use of a block position / world access
        return instance.getLightBlock();
    }
#elseif version >= 1.14
    public int getCachedOpacity() {
        // Check world access isn't required (see: Block)
        Block block = instance.getBlock();
        boolean worldAccessRequired = block#isWorldAccessRequired();
        if (worldAccessRequired) {
            return -1;
        } else {
  #if version >= 1.18
            return instance.getLightBlock((IBlockAccess) net.minecraft.world.level.BlockAccessAir.INSTANCE, BlockPosition.ZERO);
  #else
            return instance.b((IBlockAccess) net.minecraft.world.level.BlockAccessAir.INSTANCE, BlockPosition.ZERO);
  #endif
        }
    }
#elseif version >= 1.13
    public int getCachedOpacity() {
        return -1;
    }
#elseif version >= 1.9
    public int getCachedOpacity:c();
#else
    // IBlockData has no properties, delegate to Block
    public int getCachedOpacity() {
  #if version >= 1.8.3
        return instance.getBlock().p();
  #else
        return instance.getBlock().n();
  #endif
    }
#endif

#if version >= 1.18
    public boolean isPowerSource:isSignalSource();
#elseif version >= 1.13
    public boolean isPowerSource();
#elseif version >= 1.12
    public boolean isPowerSource:m();
#elseif version >= 1.11
    public boolean isPowerSource:n();
#elseif version >= 1.9
    public boolean isPowerSource:m();
#else
    public boolean isPowerSource() {
        return instance.getBlock().isPowerSource();
    }
#endif

#if version >= 1.18
    public (SoundEffectTypeHandle) SoundEffectType getSoundType()
#elseif version >= 1.16
    public (SoundEffectTypeHandle) SoundEffectType getSoundType:getStepSound()
#elseif version >= 1.14
    public (SoundEffectTypeHandle) SoundEffectType getSoundType:r()
#elseif version >= 1.9
    public (SoundEffectTypeHandle) SoundEffectType getSoundType() {
  #if version >= 1.11
        return instance.getBlock().getStepSound();
  #else
        return instance.getBlock().w();
  #endif
    }
#else
    public (SoundEffectTypeHandle) net.minecraft.world.level.block.Block.StepSound getSoundType() {
        return instance.getBlock().stepSound;
    }
#endif

    public boolean isSolid() {
#if version >= 1.20
        // Note: deprecated!
        return instance.isSolid();
#elseif version >= 1.18
        return instance.getMaterial().isSolid();
#elseif version >= 1.9
        return instance.getMaterial().isBuildable();
#else
        return instance.getBlock().getMaterial().isBuildable();
#endif
    }

#if version >= 1.13
    public (AxisAlignedBBHandle) AxisAlignedBB getBoundingBox((IBlockAccessHandle) IBlockAccess iblockaccess, (IntVector3) BlockPosition blockposition) {
  #if version >= 1.18
        VoxelShape shape = instance.getVisualShape(iblockaccess, blockposition, VoxelShapeCollision.empty());
  #elseif version >= 1.16
        VoxelShape shape = instance.getShape(iblockaccess, blockposition);
  #elseif version >= 1.14
        VoxelShape shape = instance.getBlock().a(instance, iblockaccess, blockposition, VoxelShapeCollision.a());
  #else
        VoxelShape shape = instance.getBlock().a(instance, iblockaccess, blockposition);
  #endif
  #if version >= 1.13.2
        if (shape == null || shape.isEmpty()) {
  #else
        if (shape == null || shape.b()) {
  #endif
            return null;
        } else {
  #if version >= 1.18
            return shape.bounds();
  #elseif methodexists net.minecraft.world.phys.shapes.VoxelShape public net.minecraft.world.phys.AxisAlignedBB getBoundingBox();
            return shape.getBoundingBox();
  #else
            return shape.a();
  #endif
        }
    }

#elseif version >= 1.12
    public (AxisAlignedBBHandle) AxisAlignedBB getBoundingBox:d((IBlockAccessHandle) IBlockAccess iblockaccess, (IntVector3) BlockPosition blockposition);
#elseif version >= 1.11
    public (AxisAlignedBBHandle) AxisAlignedBB getBoundingBox:c((IBlockAccessHandle) IBlockAccess iblockaccess, (IntVector3) BlockPosition blockposition);
#elseif version >= 1.9
    public (AxisAlignedBBHandle) AxisAlignedBB getBoundingBox((IBlockAccessHandle) IBlockAccess iblockaccess, (IntVector3) BlockPosition blockposition) {
        return instance.d((net.minecraft.world.level.World) iblockaccess, blockposition);
    }
#else
    public (AxisAlignedBBHandle) AxisAlignedBB getBoundingBox((IBlockAccessHandle) IBlockAccess iblockaccess, (IntVector3) BlockPosition blockposition) {
        net.minecraft.world.level.block.Block block = instance.getBlock();
        block.updateShape(iblockaccess, blockposition);
        AxisAlignedBB bounds_abs = block.a((net.minecraft.world.level.World) iblockaccess, blockposition, instance);
        if (bounds_abs == null) {
            return null;
        } else {
            // c() = add(x,y,z)  ->  make block-relative coordinates
            return bounds_abs.c((double) -blockposition.getX(), (double) -blockposition.getY(), (double) -blockposition.getZ());
        }
    }
#endif

    public Object get((IBlockStateHandle) IBlockState state) {
        if (state != null) {
#if version >= 1.18
            return instance.getValue(state);
#else
            return instance.get(state);
#endif
        } else {
            return null;
        }
    }

    public (IBlockDataHandle) IBlockData set((IBlockStateHandle) IBlockState state, Object value) {
        if (state != null) {
#if version >= 1.18
            Class type = state.getValueClass();
#elseif version >= 1.16
            Class type = state.getType();
#else
            Class type = state.b();
#endif
            Object converted = com.bergerkiller.bukkit.common.conversion.Conversion.convert(value, type, null);
            if (converted != null) {
#if version >= 1.18
                return (IBlockData) instance.setValue(state, (Comparable) converted);
#else
                return (IBlockData) instance.set(state, (Comparable) converted);
#endif
            }
        }
        return instance;
    }

#select version >=
#case 1.20.5: public (Map<IBlockStateHandle, Comparable<?>>) java.util.Map<IBlockState<?>, Comparable<?>> getStates:getValues();
#case 1.18: public abstract (Map<IBlockStateHandle, Comparable<?>>) ImmutableMap<IBlockState<?>, Comparable<?>> getStates:getValues();
#case 1.17: public abstract (Map<IBlockStateHandle, Comparable<?>>) ImmutableMap<IBlockState<?>, Comparable<?>> getStates:getStateMap();
#case 1.13.2
  #if methodexists net.minecraft.world.level.block.state.IBlockDataHolder public abstract com.google.common.collect.ImmutableMap<IBlockState<?>, Comparable<?>> getStateMap();
      public abstract (Map<IBlockStateHandle, Comparable<?>>) ImmutableMap<IBlockState<?>, Comparable<?>> getStates:getStateMap();
  #else
      public abstract (Map<IBlockStateHandle, Comparable<?>>) ImmutableMap<IBlockState<?>, Comparable<?>> getStates:b();
  #endif
#case 1.13: public abstract (Map<IBlockStateHandle, Comparable<?>>) ImmutableMap<IBlockState<?>, Comparable<?>> getStates:b();
#case 1.12: public abstract (Map<IBlockStateHandle, Comparable<?>>) ImmutableMap<IBlockState<?>, Comparable<?>> getStates:t();
#case 1.11: public abstract (Map<IBlockStateHandle, Comparable<?>>) ImmutableMap<IBlockState<?>, Comparable<?>> getStates:u();
#case 1.9:  public abstract (Map<IBlockStateHandle, Comparable<?>>) ImmutableMap<IBlockState<?>, Comparable<?>> getStates:s();
#case else: public abstract (Map<IBlockStateHandle, Comparable<?>>) ImmutableMap<IBlockState, Comparable> getStates:b();
#endselect

}