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

import net.minecraft.core.BlockPos;
import net.minecraft.core.Direction;
import net.minecraft.world.level.block.Block;
import net.minecraft.world.phys.AABB;
import net.minecraft.world.level.block.SoundType;
import net.minecraft.world.level.block.state.properties.Property;
import net.minecraft.world.level.BlockGetter;
import net.minecraft.world.level.EmptyBlockGetter;

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

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.BlockGetterHandle;
import com.bergerkiller.generated.net.minecraft.world.level.block.SoundTypeHandle;
import com.bergerkiller.generated.net.minecraft.world.level.block.state.BlockStateHandle;
import com.bergerkiller.generated.net.minecraft.world.level.block.state.properties.PropertyHandle;
import com.bergerkiller.generated.net.minecraft.world.phys.AABBHandle;

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

    <code>
    public void logProperties() {
        for (PropertyHandle property : getProperties()) {
            com.bergerkiller.bukkit.common.Logging.LOGGER.info(property + " = " + get(property));
        }
    }

    public PropertyHandle findProperty(String key) {
        for (PropertyHandle property : getProperties()) {
            if (property.getKeyToken().equals(key)) {
                return property;
            }
        }
        return null;
    }

    public BlockStateHandle set(String key, Object value) {
        return set(findProperty(key), value);
    }

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

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

    // 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 net.minecraft.world.level.block.WoolCarpetBlock) {
            return com.bergerkiller.bukkit.common.collections.BlockFaceSet.NONE;
        }

        // Check world access isn't required (see: Block)
        if (block.hasDynamicShape()) {
            return null;
        }

        // Query all 6 faces and produce a mask
        int mask = 0;
        BlockGetter emptyBlockAccess = EmptyBlockGetter.INSTANCE;
  #if version >= 1.18
        if (instance.isFaceSturdy(emptyBlockAccess, BlockPos.ZERO, Direction.NORTH)) {
            mask |= com.bergerkiller.bukkit.common.collections.BlockFaceSet.MASK_NORTH;
        }
        if (instance.isFaceSturdy(emptyBlockAccess, BlockPos.ZERO, Direction.EAST)) {
            mask |= com.bergerkiller.bukkit.common.collections.BlockFaceSet.MASK_EAST;
        }
        if (instance.isFaceSturdy(emptyBlockAccess, BlockPos.ZERO, Direction.SOUTH)) {
            mask |= com.bergerkiller.bukkit.common.collections.BlockFaceSet.MASK_SOUTH;
        }
        if (instance.isFaceSturdy(emptyBlockAccess, BlockPos.ZERO, Direction.WEST)) {
            mask |= com.bergerkiller.bukkit.common.collections.BlockFaceSet.MASK_WEST;
        }
        if (instance.isFaceSturdy(emptyBlockAccess, BlockPos.ZERO, Direction.UP)) {
            mask |= com.bergerkiller.bukkit.common.collections.BlockFaceSet.MASK_UP;
        }
        if (instance.isFaceSturdy(emptyBlockAccess, BlockPos.ZERO, Direction.DOWN)) {
            mask |= com.bergerkiller.bukkit.common.collections.BlockFaceSet.MASK_DOWN;
        }
  #elseif version >= 1.14.4
        if (instance.d(emptyBlockAccess, BlockPos.ZERO, Direction.NORTH)) {
            mask |= com.bergerkiller.bukkit.common.collections.BlockFaceSet.MASK_NORTH;
        }
        if (instance.d(emptyBlockAccess, BlockPos.ZERO, Direction.EAST)) {
            mask |= com.bergerkiller.bukkit.common.collections.BlockFaceSet.MASK_EAST;
        }
        if (instance.d(emptyBlockAccess, BlockPos.ZERO, Direction.SOUTH)) {
            mask |= com.bergerkiller.bukkit.common.collections.BlockFaceSet.MASK_SOUTH;
        }
        if (instance.d(emptyBlockAccess, BlockPos.ZERO, Direction.WEST)) {
            mask |= com.bergerkiller.bukkit.common.collections.BlockFaceSet.MASK_WEST;
        }
        if (instance.d(emptyBlockAccess, BlockPos.ZERO, Direction.UP)) {
            mask |= com.bergerkiller.bukkit.common.collections.BlockFaceSet.MASK_UP;
        }
        if (instance.d(emptyBlockAccess, BlockPos.ZERO, Direction.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, BlockPos.ZERO, Direction.NORTH)) {
            mask |= com.bergerkiller.bukkit.common.collections.BlockFaceSet.MASK_NORTH;
        }
        if (Block.d(instance, emptyBlockAccess, BlockPos.ZERO, Direction.EAST)) {
            mask |= com.bergerkiller.bukkit.common.collections.BlockFaceSet.MASK_EAST;
        }
        if (Block.d(instance, emptyBlockAccess, BlockPos.ZERO, Direction.SOUTH)) {
            mask |= com.bergerkiller.bukkit.common.collections.BlockFaceSet.MASK_SOUTH;
        }
        if (Block.d(instance, emptyBlockAccess, BlockPos.ZERO, Direction.WEST)) {
            mask |= com.bergerkiller.bukkit.common.collections.BlockFaceSet.MASK_WEST;
        }
        if (Block.d(instance, emptyBlockAccess, BlockPos.ZERO, Direction.UP)) {
            mask |= com.bergerkiller.bukkit.common.collections.BlockFaceSet.MASK_UP;
        }
        if (Block.d(instance, emptyBlockAccess, BlockPos.ZERO, Direction.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 >= 26.1
            int opacity = instance.getLightDampening();
  #elseif version >= 1.21.2
            int opacity = instance.getLightBlock();
  #elseif version >= 1.18
            int opacity = instance.getLightBlock(emptyBlockAccess, BlockPos.ZERO);
  #else
            int opacity = instance.b(emptyBlockAccess, BlockPos.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
  #if version >= 26.1
        return instance.getLightDampening();
  #else
        return instance.getLightBlock();
  #endif
    }
#elseif version >= 1.13
    public int getCachedOpacity() {
        // Check world access isn't required (see: Block)
        Block block = instance.getBlock();
        if (block.hasDynamicShape()) {
            return -1;
        } else {
  #if version >= 1.18
            return instance.getLightBlock((BlockGetter) EmptyBlockGetter.INSTANCE, BlockPos.ZERO);
  #elseif version >= 1.14
            return instance.b((BlockGetter) EmptyBlockGetter.INSTANCE, BlockPos.ZERO);
  #else
            return instance.b((BlockGetter) EmptyBlockGetter.INSTANCE, BlockPos.ZERO);
  #endif
        }
    }
#elseif version >= 1.9
    public int getCachedOpacity() {
        return Math.min(15, instance.c());
    }
#else
    // BlockState has no properties, delegate to Block
    public int getCachedOpacity() {
  #if version >= 1.8.3
        return Math.min(15, instance.getBlock().p());
  #else
        return Math.min(15, 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 (SoundTypeHandle) SoundType getSoundType()
#elseif version >= 1.16
    public (SoundTypeHandle) SoundType getSoundType:getStepSound()
#elseif version >= 1.14
    public (SoundTypeHandle) SoundType getSoundType:r()
#elseif version >= 1.9
    public (SoundTypeHandle) SoundType getSoundType() {
  #if version >= 1.11
        return instance.getBlock().getStepSound();
  #else
        return instance.getBlock().w();
  #endif
    }
#else
    public (SoundTypeHandle) net.minecraft.world.level.block.SoundType 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
    }

    // ========================================== INTERACTABLE BOX ===========================================

    public (AABBHandle) AABB getInteractableBox((BlockGetterHandle) BlockGetter iblockaccess, (IntVector3) BlockPos blockposition) {
#if version >= 1.13
  #if version >= 1.18
        VoxelShape shape = instance.getShape(iblockaccess, blockposition);
  #elseif version >= 1.16
        VoxelShape shape = instance.getShape(iblockaccess, blockposition);
  #elseif version >= 1.14
        VoxelShape shape = instance.a(iblockaccess, blockposition, CollisionContext.a());
  #elseif version >= 1.13.2
        VoxelShape shape = instance.getShape(iblockaccess, blockposition);
  #else
        VoxelShape shape = instance.g(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.AABB getBoundingBox();
            return shape.getBoundingBox();
  #else
            return shape.a();
  #endif
        }
#else
        // Before 1.13 it did not return null for air, so manually patch that
        net.minecraft.world.level.block.Block block = instance.getBlock();
        if (block == net.minecraft.world.level.block.Blocks.AIR) {
            return null;
        }

  #if version >= 1.12
        return instance.e(iblockaccess, blockposition);
  #elseif version >= 1.11
        return instance.d(iblockaccess, blockposition);
  #elseif version >= 1.9
        return instance.c((net.minecraft.world.level.Level) iblockaccess, blockposition);
  #elseif version >= 1.8.3
        block.updateShape(iblockaccess, blockposition);
        return new AABB(block.B(), block.D(), block.F(), block.C(), block.E(), block.G());
  #else
        block.updateShape(iblockaccess, blockposition);
        return new AABB(block.z(), block.B(), block.D(), block.A(), block.C(), block.E());
  #endif
#endif
    }

    // ========================================== BOUNDING BOX ===========================================

#if version >= 1.13
    public (AABBHandle) AABB getBoundingBox((BlockGetterHandle) BlockGetter iblockaccess, (IntVector3) BlockPos blockposition) {
  #if version >= 1.18
        VoxelShape shape = instance.getCollisionShape(iblockaccess, blockposition, CollisionContext.empty());
  #elseif version >= 1.16
        VoxelShape shape = instance.getCollisionShape(iblockaccess, blockposition);
  #elseif version >= 1.14
        VoxelShape shape = instance.getCollisionShape(iblockaccess, blockposition);
  #elseif version >= 1.13.2
        VoxelShape shape = instance.getCollisionShape(iblockaccess, blockposition);
  #else
        VoxelShape shape = instance.h(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.AABB getBoundingBox();
            return shape.getBoundingBox();
  #else
            return shape.a();
  #endif
        }
    }

#elseif version >= 1.12
    public (AABBHandle) AABB getBoundingBox:d((BlockGetterHandle) BlockGetter iblockaccess, (IntVector3) BlockPos blockposition);
#elseif version >= 1.11
    public (AABBHandle) AABB getBoundingBox:c((BlockGetterHandle) BlockGetter iblockaccess, (IntVector3) BlockPos blockposition);
#elseif version >= 1.9
    public (AABBHandle) AABB getBoundingBox((BlockGetterHandle) BlockGetter iblockaccess, (IntVector3) BlockPos blockposition) {
        return instance.d((net.minecraft.world.level.Level) iblockaccess, blockposition);
    }
#else
    public (AABBHandle) AABB getBoundingBox((BlockGetterHandle) BlockGetter iblockaccess, (IntVector3) BlockPos blockposition) {
        net.minecraft.world.level.block.Block block = instance.getBlock();
        block.updateShape(iblockaccess, blockposition);
        AABB bounds_abs = block.a((net.minecraft.world.level.Level) 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((PropertyHandle) Property state) {
        if (state != null) {
#if version >= 1.18
            return instance.getValue(state);
#else
            return instance.get(state);
#endif
        } else {
            return null;
        }
    }

    public (BlockStateHandle) BlockState set((PropertyHandle) Property 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 (BlockState) instance.setValue(state, (Comparable) converted);
#else
                return (BlockState) instance.set(state, (Comparable) converted);
#endif
            }
        }
        return instance;
    }

#if version >= 1.18
    public (Collection<PropertyHandle>) Collection<Property<?>> getProperties();
#elseif version >= 1.17
    public (Collection<PropertyHandle>) Collection<Property<?>> getProperties:s();
#elseif version >= 1.16
    public (Collection<PropertyHandle>) Collection<Property<?>> getProperties:r();
#elseif methodexists net.minecraft.world.level.block.state.StateHolder public abstract com.google.common.collect.ImmutableMap<net.minecraft.world.level.block.state.properties.Property<?>, Comparable<?>> getStateMap();
    // 1.13 some versions it is called getStateMap()
    public (Collection<PropertyHandle>) Collection<Property<?>> getProperties() {
        return instance.getStateMap().keySet();
    }
#else
    public (Collection<PropertyHandle>) Collection<Property<?>> getProperties() {
  #select version >=
  #case 1.13:  return instance.b().keySet();
  #case 1.12:  return instance.t().keySet();
  #case 1.11:  return instance.u().keySet();
  #case 1.9:   return instance.s().keySet();
  #case else:  return instance.b().keySet();
  #endselect
    }
#endif
}
