package net.minecraft.world.level.levelgen;

import com.mojang.serialization.Codec;
import com.mojang.serialization.DataResult;
import com.mojang.serialization.codecs.RecordCodecBuilder;
import java.util.BitSet;
import java.util.Optional;
import java.util.Set;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.stream.LongStream;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Holder;
import net.minecraft.core.registries.BuiltInRegistries;
import net.minecraft.resources.ResourceKey;
import net.minecraft.world.level.LevelHeightAccessor;
import net.minecraft.world.level.biome.Biome;
import net.minecraft.world.level.biome.BiomeResolver;
import net.minecraft.world.level.biome.Biomes;
import net.minecraft.world.level.block.Blocks;
import net.minecraft.world.level.chunk.ChunkAccess;
import net.minecraft.world.level.chunk.ProtoChunk;
import net.minecraft.world.level.chunk.status.ChunkStatus;

public final class BelowZeroRetrogen {
    private static final BitSet EMPTY = new BitSet(0);
    private static final Codec<BitSet> BITSET_CODEC = Codec.LONG_STREAM
        .xmap(bitset -> BitSet.valueOf(bitset.toArray()), bitset -> LongStream.of(bitset.toLongArray()));
    private static final Codec<ChunkStatus> NON_EMPTY_CHUNK_STATUS = BuiltInRegistries.CHUNK_STATUS
        .byNameCodec()
        .comapFlatMap(
            chunkStatus -> chunkStatus == ChunkStatus.EMPTY
                ? DataResult.error(() -> "target_status cannot be empty")
                : DataResult.success((ChunkStatus)chunkStatus),
            Function.identity()
        );
    public static final Codec<BelowZeroRetrogen> CODEC = RecordCodecBuilder.create(
        instance -> instance.group(
                NON_EMPTY_CHUNK_STATUS.fieldOf("target_status").forGetter(BelowZeroRetrogen::targetStatus),
                BITSET_CODEC.lenientOptionalFieldOf("missing_bedrock")
                    .forGetter(
                        belowZeroRetrogen -> belowZeroRetrogen.missingBedrock.isEmpty() ? Optional.empty() : Optional.of(belowZeroRetrogen.missingBedrock)
                    )
            )
            .apply(instance, BelowZeroRetrogen::new)
    );
    private static final Set<ResourceKey<Biome>> RETAINED_RETROGEN_BIOMES = Set.of(Biomes.LUSH_CAVES, Biomes.DRIPSTONE_CAVES, Biomes.DEEP_DARK);
    public static final LevelHeightAccessor UPGRADE_HEIGHT_ACCESSOR = new LevelHeightAccessor() {
        @Override
        public int getHeight() {
            return 64;
        }

        @Override
        public int getMinY() {
            return -64;
        }
    };
    private final ChunkStatus targetStatus;
    private final BitSet missingBedrock;

    private BelowZeroRetrogen(ChunkStatus targetStatus, Optional<BitSet> missingBedrock) {
        this.targetStatus = targetStatus;
        this.missingBedrock = missingBedrock.orElse(EMPTY);
    }

    public static void replaceOldBedrock(ProtoChunk chunk) {
        int i = 4;
        BlockPos.betweenClosed(0, 0, 0, 15, 4, 15).forEach(blockPos -> {
            if (chunk.getBlockState(blockPos).is(Blocks.BEDROCK)) {
                chunk.setBlockState(blockPos, Blocks.DEEPSLATE.defaultBlockState());
            }
        });
    }

    public void applyBedrockMask(ProtoChunk chunk) {
        LevelHeightAccessor heightAccessorForGeneration = chunk.getHeightAccessorForGeneration();
        int minY = heightAccessorForGeneration.getMinY();
        int maxY = heightAccessorForGeneration.getMaxY();

        for (int i = 0; i < 16; i++) {
            for (int i1 = 0; i1 < 16; i1++) {
                if (this.hasBedrockHole(i, i1)) {
                    BlockPos.betweenClosed(i, minY, i1, i, maxY, i1).forEach(blockPos -> chunk.setBlockState(blockPos, Blocks.AIR.defaultBlockState()));
                }
            }
        }
    }

    public ChunkStatus targetStatus() {
        return this.targetStatus;
    }

    public boolean hasBedrockHoles() {
        return !this.missingBedrock.isEmpty();
    }

    public boolean hasBedrockHole(int x, int z) {
        return this.missingBedrock.get((z & 15) * 16 + (x & 15));
    }

    public static BiomeResolver getBiomeResolver(BiomeResolver resolver, ChunkAccess access) {
        if (!access.isUpgrading()) {
            return resolver;
        } else {
            Predicate<ResourceKey<Biome>> predicate = RETAINED_RETROGEN_BIOMES::contains;
            return (x, y, z, sampler) -> {
                Holder<Biome> noiseBiome = resolver.getNoiseBiome(x, y, z, sampler);
                return noiseBiome.is(predicate) ? noiseBiome : access.getNoiseBiome(x, 0, z);
            };
        }
    }
}
