package net.minecraft.world.level.material;

import net.minecraft.world.level.block.state.properties.BlockStateProperties;
import net.minecraft.world.level.Level;
import net.minecraft.world.level.block.Blocks;
import net.minecraft.tags.BlockTags;
import net.minecraft.world.level.block.DoorBlock;
import it.unimi.dsi.fastutil.shorts.Short2BooleanOpenHashMap;
import it.unimi.dsi.fastutil.shorts.Short2ObjectOpenHashMap;
import it.unimi.dsi.fastutil.shorts.Short2BooleanMap;
import com.mojang.datafixers.util.Pair;
import it.unimi.dsi.fastutil.shorts.Short2ObjectMap;
import net.minecraft.world.level.block.LiquidBlockContainer;
import net.minecraft.world.phys.shapes.Shapes;
import net.minecraft.world.level.LevelReader;
import net.minecraft.world.level.LevelAccessor;
import net.minecraft.world.level.block.state.BlockState;
import java.util.Iterator;
import net.minecraft.core.Vec3i;
import net.minecraft.core.Direction;
import net.minecraft.world.phys.Vec3;
import net.minecraft.core.BlockPos;
import net.minecraft.world.level.BlockGetter;
import net.minecraft.world.level.block.state.properties.Property;
import net.minecraft.world.level.block.state.StateDefinition;
import com.google.common.collect.Maps;
import net.minecraft.world.phys.shapes.VoxelShape;
import java.util.Map;
import net.minecraft.world.level.block.Block;
import it.unimi.dsi.fastutil.objects.Object2ByteLinkedOpenHashMap;
import net.minecraft.world.level.block.state.properties.IntegerProperty;
import net.minecraft.world.level.block.state.properties.BooleanProperty;

public abstract class FlowingFluid extends Fluid {
    public static final BooleanProperty FALLING;
    public static final IntegerProperty LEVEL;
    private static final ThreadLocal<Object2ByteLinkedOpenHashMap<Block.BlockStatePairKey>> OCCLUSION_CACHE;
    private final Map<FluidState, VoxelShape> shapes;
    
    public FlowingFluid() {
        this.shapes = Maps.newIdentityHashMap();
    }
    
    @Override
    protected void createFluidStateDefinition(final StateDefinition.Builder<Fluid, FluidState> a) {
        a.add(FlowingFluid.FALLING);
    }
    
    public Vec3 getFlow(final BlockGetter bjg, final BlockPos fk, final FluidState coj) {
        double double5 = 0.0;
        double double6 = 0.0;
        try (final BlockPos.PooledMutableBlockPos b9 = BlockPos.PooledMutableBlockPos.acquire()) {
            for (final Direction fp12 : Direction.Plane.HORIZONTAL) {
                b9.set((Vec3i)fk).move(fp12);
                final FluidState coj2 = bjg.getFluidState(b9);
                if (!this.affectsFlow(coj2)) {
                    continue;
                }
                float float14 = coj2.getOwnHeight();
                float float15 = 0.0f;
                if (float14 == 0.0f) {
                    if (!bjg.getBlockState(b9).getMaterial().blocksMotion()) {
                        final BlockPos fk2 = b9.below();
                        final FluidState coj3 = bjg.getFluidState(fk2);
                        if (this.affectsFlow(coj3)) {
                            float14 = coj3.getOwnHeight();
                            if (float14 > 0.0f) {
                                float15 = coj.getOwnHeight() - (float14 - 0.8888889f);
                            }
                        }
                    }
                }
                else if (float14 > 0.0f) {
                    float15 = coj.getOwnHeight() - float14;
                }
                if (float15 == 0.0f) {
                    continue;
                }
                double5 += fp12.getStepX() * float15;
                double6 += fp12.getStepZ() * float15;
            }
            Vec3 cvl11 = new Vec3(double5, 0.0, double6);
            if (coj.<Boolean>getValue((Property<Boolean>)FlowingFluid.FALLING)) {
                for (final Direction fp13 : Direction.Plane.HORIZONTAL) {
                    b9.set((Vec3i)fk).move(fp13);
                    if (this.isSolidFace(bjg, b9, fp13) || this.isSolidFace(bjg, b9.above(), fp13)) {
                        cvl11 = cvl11.normalize().add(0.0, -6.0, 0.0);
                        break;
                    }
                }
            }
            return cvl11.normalize();
        }
    }
    
    private boolean affectsFlow(final FluidState coj) {
        return coj.isEmpty() || coj.getType().isSame(this);
    }
    
    protected boolean isSolidFace(final BlockGetter bjg, final BlockPos fk, final Direction fp) {
        final BlockState byj5 = bjg.getBlockState(fk);
        final FluidState coj6 = bjg.getFluidState(fk);
        return !coj6.getType().isSame(this) && (fp == Direction.UP || (byj5.getMaterial() != Material.ICE && byj5.isFaceSturdy(bjg, fk, fp)));
    }
    
    protected void spread(final LevelAccessor bjx, final BlockPos fk, final FluidState coj) {
        if (coj.isEmpty()) {
            return;
        }
        final BlockState byj5 = bjx.getBlockState(fk);
        final BlockPos fk2 = fk.below();
        final BlockState byj6 = bjx.getBlockState(fk2);
        final FluidState coj2 = this.getNewLiquid(bjx, fk2, byj6);
        if (this.canSpreadTo(bjx, fk, byj5, Direction.DOWN, fk2, byj6, bjx.getFluidState(fk2), coj2.getType())) {
            this.spreadTo(bjx, fk2, byj6, Direction.DOWN, coj2);
            if (this.sourceNeighborCount(bjx, fk) >= 3) {
                this.spreadToSides(bjx, fk, coj, byj5);
            }
        }
        else if (coj.isSource() || !this.isWaterHole(bjx, coj2.getType(), fk, byj5, fk2, byj6)) {
            this.spreadToSides(bjx, fk, coj, byj5);
        }
    }
    
    private void spreadToSides(final LevelAccessor bjx, final BlockPos fk, final FluidState coj, final BlockState byj) {
        int integer6 = coj.getAmount() - this.getDropOff(bjx);
        if (coj.<Boolean>getValue((Property<Boolean>)FlowingFluid.FALLING)) {
            integer6 = 7;
        }
        if (integer6 <= 0) {
            return;
        }
        final Map<Direction, FluidState> map7 = this.getSpread(bjx, fk, byj);
        for (final Map.Entry<Direction, FluidState> entry9 : map7.entrySet()) {
            final Direction fp10 = entry9.getKey();
            final FluidState coj2 = entry9.getValue();
            final BlockPos fk2 = fk.relative(fp10);
            final BlockState byj2 = bjx.getBlockState(fk2);
            if (this.canSpreadTo(bjx, fk, byj, fp10, fk2, byj2, bjx.getFluidState(fk2), coj2.getType())) {
                this.spreadTo(bjx, fk2, byj2, fp10, coj2);
            }
        }
    }
    
    protected FluidState getNewLiquid(final LevelReader bjz, final BlockPos fk, final BlockState byj) {
        int integer5 = 0;
        int integer6 = 0;
        for (final Direction fp8 : Direction.Plane.HORIZONTAL) {
            final BlockPos fk2 = fk.relative(fp8);
            final BlockState byj2 = bjz.getBlockState(fk2);
            final FluidState coj11 = byj2.getFluidState();
            if (coj11.getType().isSame(this) && this.canPassThroughWall(fp8, bjz, fk, byj, fk2, byj2)) {
                if (coj11.isSource()) {
                    ++integer6;
                }
                integer5 = Math.max(integer5, coj11.getAmount());
            }
        }
        if (this.canConvertToSource() && integer6 >= 2) {
            final BlockState byj3 = bjz.getBlockState(fk.below());
            final FluidState coj12 = byj3.getFluidState();
            if (byj3.getMaterial().isSolid() || this.isSourceBlockOfThisType(coj12)) {
                return this.getSource(false);
            }
        }
        final BlockPos fk3 = fk.above();
        final BlockState byj4 = bjz.getBlockState(fk3);
        final FluidState coj13 = byj4.getFluidState();
        if (!coj13.isEmpty() && coj13.getType().isSame(this) && this.canPassThroughWall(Direction.UP, bjz, fk, byj, fk3, byj4)) {
            return this.getFlowing(8, true);
        }
        final int integer7 = integer5 - this.getDropOff(bjz);
        if (integer7 <= 0) {
            return Fluids.EMPTY.defaultFluidState();
        }
        return this.getFlowing(integer7, false);
    }
    
    private boolean canPassThroughWall(final Direction fp, final BlockGetter bjg, final BlockPos fk3, final BlockState byj4, final BlockPos fk5, final BlockState byj6) {
        Object2ByteLinkedOpenHashMap<Block.BlockStatePairKey> object2ByteLinkedOpenHashMap8;
        if (byj4.getBlock().hasDynamicShape() || byj6.getBlock().hasDynamicShape()) {
            object2ByteLinkedOpenHashMap8 = null;
        }
        else {
            object2ByteLinkedOpenHashMap8 = FlowingFluid.OCCLUSION_CACHE.get();
        }
        Block.BlockStatePairKey a9;
        if (object2ByteLinkedOpenHashMap8 != null) {
            a9 = new Block.BlockStatePairKey(byj4, byj6, fp);
            final byte byte10 = object2ByteLinkedOpenHashMap8.getAndMoveToFirst(a9);
            if (byte10 != 127) {
                return byte10 != 0;
            }
        }
        else {
            a9 = null;
        }
        final VoxelShape cwf10 = byj4.getCollisionShape(bjg, fk3);
        final VoxelShape cwf11 = byj6.getCollisionShape(bjg, fk5);
        final boolean boolean12 = !Shapes.mergedFaceOccludes(cwf10, cwf11, fp);
        if (object2ByteLinkedOpenHashMap8 != null) {
            if (object2ByteLinkedOpenHashMap8.size() == 200) {
                object2ByteLinkedOpenHashMap8.removeLastByte();
            }
            object2ByteLinkedOpenHashMap8.putAndMoveToFirst(a9, (byte)(byte)(boolean12 ? 1 : 0));
        }
        return boolean12;
    }
    
    public abstract Fluid getFlowing();
    
    public FluidState getFlowing(final int integer, final boolean boolean2) {
        return this.getFlowing().defaultFluidState().<Comparable, Integer>setValue((Property<Comparable>)FlowingFluid.LEVEL, integer).<Comparable, Boolean>setValue((Property<Comparable>)FlowingFluid.FALLING, boolean2);
    }
    
    public abstract Fluid getSource();
    
    public FluidState getSource(final boolean boolean1) {
        return this.getSource().defaultFluidState().<Comparable, Boolean>setValue((Property<Comparable>)FlowingFluid.FALLING, boolean1);
    }
    
    protected abstract boolean canConvertToSource();
    
    protected void spreadTo(final LevelAccessor bjx, final BlockPos fk, final BlockState byj, final Direction fp, final FluidState coj) {
        if (byj.getBlock() instanceof LiquidBlockContainer) {
            ((LiquidBlockContainer)byj.getBlock()).placeLiquid(bjx, fk, byj, coj);
        }
        else {
            if (!byj.isAir()) {
                this.beforeDestroyingBlock(bjx, fk, byj);
            }
            bjx.setBlock(fk, coj.createLegacyBlock(), 3);
        }
    }
    
    protected abstract void beforeDestroyingBlock(final LevelAccessor bjx, final BlockPos fk, final BlockState byj);
    
    private static short getCacheKey(final BlockPos fk1, final BlockPos fk2) {
        final int integer3 = fk2.getX() - fk1.getX();
        final int integer4 = fk2.getZ() - fk1.getZ();
        return (short)((integer3 + 128 & 0xFF) << 8 | (integer4 + 128 & 0xFF));
    }
    
    protected int getSlopeDistance(final LevelReader bjz, final BlockPos fk2, final int integer, final Direction fp, final BlockState byj, final BlockPos fk6, final Short2ObjectMap<Pair<BlockState, FluidState>> short2ObjectMap, final Short2BooleanMap short2BooleanMap) {
        int integer2 = 1000;
        for (final Direction fp2 : Direction.Plane.HORIZONTAL) {
            if (fp2 == fp) {
                continue;
            }
            final BlockPos fk7 = fk2.relative(fp2);
            final short short14 = getCacheKey(fk6, fk7);
            final BlockState byj2;
            final Pair<BlockState, FluidState> pair15 = (Pair<BlockState, FluidState>)short2ObjectMap.computeIfAbsent(short14, integer -> {
                byj2 = bjz.getBlockState(fk7);
                return Pair.of(byj2, byj2.getFluidState());
            });
            final BlockState byj3 = (BlockState)pair15.getFirst();
            final FluidState coj17 = (FluidState)pair15.getSecond();
            if (!this.canPassThrough(bjz, this.getFlowing(), fk2, byj, fp2, fk7, byj3, coj17)) {
                continue;
            }
            final BlockPos fk9;
            final BlockPos fk8;
            final BlockState byj4;
            final BlockState byj5;
            final boolean boolean18 = short2BooleanMap.computeIfAbsent(short14, integer -> {
                fk8 = fk9.below();
                byj4 = bjz.getBlockState(fk8);
                return this.isWaterHole(bjz, this.getFlowing(), fk9, byj5, fk8, byj4);
            });
            if (boolean18) {
                return integer;
            }
            if (integer >= this.getSlopeFindDistance(bjz)) {
                continue;
            }
            final int integer3 = this.getSlopeDistance(bjz, fk7, integer + 1, fp2.getOpposite(), byj3, fk6, short2ObjectMap, short2BooleanMap);
            if (integer3 >= integer2) {
                continue;
            }
            integer2 = integer3;
        }
        return integer2;
    }
    
    private boolean isWaterHole(final BlockGetter bjg, final Fluid coi, final BlockPos fk3, final BlockState byj4, final BlockPos fk5, final BlockState byj6) {
        return this.canPassThroughWall(Direction.DOWN, bjg, fk3, byj4, fk5, byj6) && (byj6.getFluidState().getType().isSame(this) || this.canHoldFluid(bjg, fk5, byj6, coi));
    }
    
    private boolean canPassThrough(final BlockGetter bjg, final Fluid coi, final BlockPos fk3, final BlockState byj4, final Direction fp, final BlockPos fk6, final BlockState byj7, final FluidState coj) {
        return !this.isSourceBlockOfThisType(coj) && this.canPassThroughWall(fp, bjg, fk3, byj4, fk6, byj7) && this.canHoldFluid(bjg, fk6, byj7, coi);
    }
    
    private boolean isSourceBlockOfThisType(final FluidState coj) {
        return coj.getType().isSame(this) && coj.isSource();
    }
    
    protected abstract int getSlopeFindDistance(final LevelReader bjz);
    
    private int sourceNeighborCount(final LevelReader bjz, final BlockPos fk) {
        int integer4 = 0;
        for (final Direction fp6 : Direction.Plane.HORIZONTAL) {
            final BlockPos fk2 = fk.relative(fp6);
            final FluidState coj8 = bjz.getFluidState(fk2);
            if (this.isSourceBlockOfThisType(coj8)) {
                ++integer4;
            }
        }
        return integer4;
    }
    
    protected Map<Direction, FluidState> getSpread(final LevelReader bjz, final BlockPos fk, final BlockState byj) {
        int integer5 = 1000;
        final Map<Direction, FluidState> map6 = Maps.newEnumMap(Direction.class);
        final Short2ObjectMap<Pair<BlockState, FluidState>> short2ObjectMap7 = (Short2ObjectMap<Pair<BlockState, FluidState>>)new Short2ObjectOpenHashMap();
        final Short2BooleanMap short2BooleanMap8 = (Short2BooleanMap)new Short2BooleanOpenHashMap();
        for (final Direction fp10 : Direction.Plane.HORIZONTAL) {
            final BlockPos fk2 = fk.relative(fp10);
            final short short12 = getCacheKey(fk, fk2);
            final BlockState byj2;
            final Pair<BlockState, FluidState> pair13 = (Pair<BlockState, FluidState>)short2ObjectMap7.computeIfAbsent(short12, integer -> {
                byj2 = bjz.getBlockState(fk2);
                return Pair.of(byj2, byj2.getFluidState());
            });
            final BlockState byj3 = (BlockState)pair13.getFirst();
            final FluidState coj15 = (FluidState)pair13.getSecond();
            final FluidState coj16 = this.getNewLiquid(bjz, fk2, byj3);
            if (this.canPassThrough(bjz, coj16.getType(), fk, byj, fp10, fk2, byj3, coj15)) {
                final BlockPos fk3 = fk2.below();
                final BlockPos blockPos;
                final BlockState byj4;
                final BlockPos fk4;
                final BlockState byj5;
                final boolean boolean19 = short2BooleanMap8.computeIfAbsent(short12, integer -> {
                    byj4 = bjz.getBlockState(blockPos);
                    return this.isWaterHole(bjz, this.getFlowing(), fk4, byj5, blockPos, byj4);
                });
                int integer6;
                if (boolean19) {
                    integer6 = 0;
                }
                else {
                    integer6 = this.getSlopeDistance(bjz, fk2, 1, fp10.getOpposite(), byj3, fk, short2ObjectMap7, short2BooleanMap8);
                }
                if (integer6 < integer5) {
                    map6.clear();
                }
                if (integer6 > integer5) {
                    continue;
                }
                map6.put(fp10, coj16);
                integer5 = integer6;
            }
        }
        return map6;
    }
    
    private boolean canHoldFluid(final BlockGetter bjg, final BlockPos fk, final BlockState byj, final Fluid coi) {
        final Block bph6 = byj.getBlock();
        if (bph6 instanceof LiquidBlockContainer) {
            return ((LiquidBlockContainer)bph6).canPlaceLiquid(bjg, fk, byj, coi);
        }
        if (bph6 instanceof DoorBlock || bph6.is(BlockTags.SIGNS) || bph6 == Blocks.LADDER || bph6 == Blocks.SUGAR_CANE || bph6 == Blocks.BUBBLE_COLUMN) {
            return false;
        }
        final Material coo7 = byj.getMaterial();
        return coo7 != Material.PORTAL && coo7 != Material.STRUCTURAL_AIR && coo7 != Material.WATER_PLANT && coo7 != Material.REPLACEABLE_WATER_PLANT && !coo7.blocksMotion();
    }
    
    protected boolean canSpreadTo(final BlockGetter bjg, final BlockPos fk2, final BlockState byj3, final Direction fp, final BlockPos fk5, final BlockState byj6, final FluidState coj, final Fluid coi) {
        return coj.canBeReplacedWith(bjg, fk5, coi, fp) && this.canPassThroughWall(fp, bjg, fk2, byj3, fk5, byj6) && this.canHoldFluid(bjg, fk5, byj6, coi);
    }
    
    protected abstract int getDropOff(final LevelReader bjz);
    
    protected int getSpreadDelay(final Level bjw, final BlockPos fk, final FluidState coj3, final FluidState coj4) {
        return this.getTickDelay(bjw);
    }
    
    public void tick(final Level bjw, final BlockPos fk, FluidState coj) {
        if (!coj.isSource()) {
            final FluidState coj2 = this.getNewLiquid(bjw, fk, bjw.getBlockState(fk));
            final int integer6 = this.getSpreadDelay(bjw, fk, coj, coj2);
            if (coj2.isEmpty()) {
                coj = coj2;
                bjw.setBlock(fk, Blocks.AIR.defaultBlockState(), 3);
            }
            else if (!coj2.equals(coj)) {
                coj = coj2;
                final BlockState byj7 = coj.createLegacyBlock();
                bjw.setBlock(fk, byj7, 2);
                bjw.getLiquidTicks().scheduleTick(fk, coj.getType(), integer6);
                bjw.updateNeighborsAt(fk, byj7.getBlock());
            }
        }
        this.spread(bjw, fk, coj);
    }
    
    protected static int getLegacyLevel(final FluidState coj) {
        if (coj.isSource()) {
            return 0;
        }
        return 8 - Math.min(coj.getAmount(), 8) + (coj.<Boolean>getValue((Property<Boolean>)FlowingFluid.FALLING) ? 8 : 0);
    }
    
    private static boolean hasSameAbove(final FluidState coj, final BlockGetter bjg, final BlockPos fk) {
        return coj.getType().isSame(bjg.getFluidState(fk.above()).getType());
    }
    
    @Override
    public float getHeight(final FluidState coj, final BlockGetter bjg, final BlockPos fk) {
        if (hasSameAbove(coj, bjg, fk)) {
            return 1.0f;
        }
        return coj.getOwnHeight();
    }
    
    @Override
    public float getOwnHeight(final FluidState coj) {
        return coj.getAmount() / 9.0f;
    }
    
    @Override
    public VoxelShape getShape(final FluidState coj, final BlockGetter bjg, final BlockPos fk) {
        if (coj.getAmount() == 9 && hasSameAbove(coj, bjg, fk)) {
            return Shapes.block();
        }
        return this.shapes.computeIfAbsent(coj, coj -> Shapes.box(0.0, 0.0, 0.0, 1.0, coj.getHeight(bjg, fk), 1.0));
    }
    
    static {
        FALLING = BlockStateProperties.FALLING;
        LEVEL = BlockStateProperties.LEVEL_FLOWING;
        final Object2ByteLinkedOpenHashMap<Block.BlockStatePairKey> object2ByteLinkedOpenHashMap1;
        OCCLUSION_CACHE = ThreadLocal.<Object2ByteLinkedOpenHashMap<Block.BlockStatePairKey>>withInitial(() -> {
            object2ByteLinkedOpenHashMap1 = new Object2ByteLinkedOpenHashMap<Block.BlockStatePairKey>(200) {
                protected void rehash(final int integer) {
                }
            };
            object2ByteLinkedOpenHashMap1.defaultReturnValue((byte)127);
            return object2ByteLinkedOpenHashMap1;
        });
    }
}
