package fr.koka99cab.sanctuary.worldgen.feature;

import com.mojang.serialization.Codec;
import net.minecraft.core.BlockPos;
import net.minecraft.world.level.WorldGenLevel;
import net.minecraft.world.level.block.Block;
import net.minecraft.world.level.block.Blocks;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.levelgen.feature.Feature;
import net.minecraft.world.level.levelgen.feature.FeaturePlaceContext;
import net.minecraft.world.level.levelgen.feature.configurations.NoneFeatureConfiguration;

public class MatterDepthMaterializationFeature extends Feature<NoneFeatureConfiguration> {
	private static final int MIN_Y = 0;
	private static final int MAX_Y = 255;
	private static final int HEIGHT = MAX_Y - MIN_Y + 1;
	private static final int CHUNK_WIDTH = 16;
	private static final int CHUNK_AREA = CHUNK_WIDTH * CHUNK_WIDTH;
	private static final int CHUNK_VOLUME = CHUNK_AREA * HEIGHT;
	private static final int MAX_MATTER_DEPTH = 18;

	public MatterDepthMaterializationFeature(Codec<NoneFeatureConfiguration> codec) {
		super(codec);
	}

	@Override
	public boolean place(FeaturePlaceContext<NoneFeatureConfiguration> context) {
		if (!SanctuaryWorldgenChecks.isSanctuaryGenerator(context)) {
			return false;
		}

		WorldGenLevel level = context.level();
		BlockPos origin = context.origin();
		int chunkStartX = origin.getX() & ~15;
		int chunkStartZ = origin.getZ() & ~15;
		BlockPos.MutableBlockPos mutable = new BlockPos.MutableBlockPos(0, 0, 0);
		BlockState[] states = new BlockState[CHUNK_VOLUME];
		int[] matterDepths = new int[CHUNK_VOLUME];
		boolean changed = false;

		for (int localX = 0; localX < 16; localX++) {
			int x = chunkStartX + localX;
			for (int localZ = 0; localZ < 16; localZ++) {
				int z = chunkStartZ + localZ;
				for (int y = MIN_Y; y <= MAX_Y; y++) {
					mutable.set(x, y, z);
					int index = index(localX, y - MIN_Y, localZ);
					BlockState state = level.getBlockState(mutable);
					states[index] = state;
					matterDepths[index] = state.isAir() ? 0 : MAX_MATTER_DEPTH;
				}
			}
		}

		computeMatterDepths(matterDepths);

		for (int localX = 0; localX < 16; localX++) {
			int x = chunkStartX + localX;
			for (int localZ = 0; localZ < 16; localZ++) {
				int z = chunkStartZ + localZ;
				for (int y = MIN_Y; y <= MAX_Y; y++) {
					int index = index(localX, y - MIN_Y, localZ);
					BlockState current = states[index];
					if (current.getBlock() != Blocks.STONE) {
						continue;
					}

					BlockState replacement = replacementFor(x, y, z, matterDepths[index]);
					if (replacement != null && replacement.getBlock() != current.getBlock()) {
						mutable.set(x, y, z);
						level.setBlock(mutable, replacement, Block.UPDATE_CLIENTS);
						changed = true;
					}
				}
			}
		}

		return changed;
	}

	private static void computeMatterDepths(int[] matterDepths) {
		for (int y = 0; y < HEIGHT; y++) {
			for (int z = 0; z < CHUNK_WIDTH; z++) {
				for (int x = 0; x < CHUNK_WIDTH; x++) {
					int index = index(x, y, z);
					if (matterDepths[index] == 0) {
						continue;
					}
					int depth = matterDepths[index];
					if (x == 0 || z == 0 || y == 0) {
						depth = 1;
					} else {
						depth = Math.min(depth, matterDepths[index(x - 1, y, z)] + 1);
						depth = Math.min(depth, matterDepths[index(x, y, z - 1)] + 1);
						depth = Math.min(depth, matterDepths[index(x, y - 1, z)] + 1);
					}
					matterDepths[index] = Math.min(depth, MAX_MATTER_DEPTH);
				}
			}
		}

		for (int y = HEIGHT - 1; y >= 0; y--) {
			for (int z = CHUNK_WIDTH - 1; z >= 0; z--) {
				for (int x = CHUNK_WIDTH - 1; x >= 0; x--) {
					int index = index(x, y, z);
					if (matterDepths[index] == 0) {
						continue;
					}
					int depth = matterDepths[index];
					if (x == CHUNK_WIDTH - 1 || z == CHUNK_WIDTH - 1 || y == HEIGHT - 1) {
						depth = Math.min(depth, 1);
					} else {
						depth = Math.min(depth, matterDepths[index(x + 1, y, z)] + 1);
						depth = Math.min(depth, matterDepths[index(x, y, z + 1)] + 1);
						depth = Math.min(depth, matterDepths[index(x, y + 1, z)] + 1);
					}
					matterDepths[index] = Math.min(depth, MAX_MATTER_DEPTH);
				}
			}
		}
	}

	private static int index(int x, int y, int z) {
		return (y * CHUNK_WIDTH + z) * CHUNK_WIDTH + x;
	}

	private static BlockState replacementFor(int x, int y, int z, int matterDepth) {
		if (matterDepth <= 4) {
			return null;
		}

		double compressionNoise = smoothNoise3d(x * 0.070 + 9.0, y * 0.045 - 13.0, z * 0.070 + 31.0);
		double seamNoise = smoothNoise3d(x * 0.145 - 41.0, y * 0.115 + 3.0, z * 0.145 + 17.0);
		double pressureDepth = matterDepth + compressionNoise * 4.2 + seamNoise * 1.8;

		if (pressureDepth >= 8.8 && isDiamondPocket(x, y, z, pressureDepth)) {
			return Blocks.DEEPSLATE_DIAMOND_ORE.defaultBlockState();
		}
		if (pressureDepth >= 9.8) {
			if (seamNoise < -0.74 && hash01(x, y, z, 19) < 0.24) {
				return Blocks.TUFF.defaultBlockState();
			}
			return Blocks.DEEPSLATE.defaultBlockState();
		}
		if (pressureDepth >= 6.6) {
			if (compressionNoise + seamNoise * 0.45 > -0.12) {
				return Blocks.DEEPSLATE.defaultBlockState();
			}
			if (seamNoise > -0.58) {
				return Blocks.TUFF.defaultBlockState();
			}
			return null;
		}
		if (pressureDepth >= 4.8 && seamNoise > 0.18) {
			return Blocks.TUFF.defaultBlockState();
		}

		return null;
	}

	private static boolean isDiamondPocket(int x, int y, int z, double pressureDepth) {
		double pressure = clamp((pressureDepth - 8.8) / 5.4, 0.0, 1.0);
		double oreNoise = smoothNoise3d(x * 0.118 + 71.0, y * 0.126 - 23.0, z * 0.118 + 5.0)
			+ 0.38 * smoothNoise3d(x * 0.255 - 11.0, y * 0.210 + 37.0, z * 0.255 - 29.0);
		double threshold = 0.48 - pressure * 0.08;
		double thinning = 0.160 + pressure * 0.065;
		return oreNoise > threshold && hash01(x, y, z, 73) < thinning;
	}

	private static double smoothNoise3d(double x, double y, double z) {
		int floorX = (int) Math.floor(x);
		int floorY = (int) Math.floor(y);
		int floorZ = (int) Math.floor(z);
		double localX = x - floorX;
		double localY = y - floorY;
		double localZ = z - floorZ;
		double fadeX = smoothStep(localX);
		double fadeY = smoothStep(localY);
		double fadeZ = smoothStep(localZ);

		double x00 = lerp(hashToNoise(floorX, floorY, floorZ), hashToNoise(floorX + 1, floorY, floorZ), fadeX);
		double x10 = lerp(hashToNoise(floorX, floorY + 1, floorZ), hashToNoise(floorX + 1, floorY + 1, floorZ), fadeX);
		double x01 = lerp(hashToNoise(floorX, floorY, floorZ + 1), hashToNoise(floorX + 1, floorY, floorZ + 1), fadeX);
		double x11 = lerp(hashToNoise(floorX, floorY + 1, floorZ + 1), hashToNoise(floorX + 1, floorY + 1, floorZ + 1), fadeX);
		double y0 = lerp(x00, x10, fadeY);
		double y1 = lerp(x01, x11, fadeY);
		return lerp(y0, y1, fadeZ);
	}

	private static double smoothStep(double value) {
		return value * value * (3.0 - 2.0 * value);
	}

	private static double lerp(double first, double second, double delta) {
		return first + (second - first) * delta;
	}

	private static double hashToNoise(int x, int y, int z) {
		return hash01(x, y, z, 0) * 2.0 - 1.0;
	}

	private static double hash01(int x, int y, int z, int salt) {
		long value = 0x9E3779B97F4A7C15L;
		value ^= (long) x * 0xBF58476D1CE4E5B9L;
		value ^= (long) y * 0x94D049BB133111EBL;
		value ^= (long) z * 0xD6E8FEB86659FD93L;
		value ^= (long) salt * 0x9E3779B185EBCA87L;
		value = (value ^ (value >>> 30)) * 0xBF58476D1CE4E5B9L;
		value = (value ^ (value >>> 27)) * 0x94D049BB133111EBL;
		value = value ^ (value >>> 31);
		return (value >>> 11) * 0x1.0p-53;
	}

	private static double clamp(double value, double min, double max) {
		return Math.max(min, Math.min(max, value));
	}
}
