package fr.koka99cab.sanctuary.worldgen;

import com.mojang.serialization.Codec;
import com.mojang.serialization.MapCodec;
import com.mojang.serialization.codecs.RecordCodecBuilder;
import fr.koka99cab.sanctuary.SanctuaryMod;
import net.minecraft.core.Registry;
import net.minecraft.core.registries.BuiltInRegistries;
import net.minecraft.resources.Identifier;
import net.minecraft.util.KeyDispatchDataCodec;
import net.minecraft.world.level.levelgen.DensityFunction;

public final class SanctuaryDensityFunctions {
	private SanctuaryDensityFunctions() {
	}

	public static void register() {
		Registry.register(
			BuiltInRegistries.DENSITY_FUNCTION_TYPE,
			Identifier.fromNamespaceAndPath(SanctuaryMod.MOD_ID, "radial_void_mask"),
			RadialVoidMask.DATA_CODEC
		);
		Registry.register(
			BuiltInRegistries.DENSITY_FUNCTION_TYPE,
			Identifier.fromNamespaceAndPath(SanctuaryMod.MOD_ID, "island_spawn_density"),
			IslandSpawnDensity.DATA_CODEC
		);
	}

	public record RadialVoidMask(
		int voidRadius,
		int transitionRadius,
		double insideValue,
		double outsideValue
	) implements DensityFunction.SimpleFunction {
		public static final MapCodec<RadialVoidMask> DATA_CODEC = RecordCodecBuilder.mapCodec(instance -> instance.group(
			Codec.INT.fieldOf("void_radius").forGetter(RadialVoidMask::voidRadius),
			Codec.INT.fieldOf("transition_radius").forGetter(RadialVoidMask::transitionRadius),
			Codec.DOUBLE.fieldOf("inside_value").forGetter(RadialVoidMask::insideValue),
			Codec.DOUBLE.fieldOf("outside_value").forGetter(RadialVoidMask::outsideValue)
		).apply(instance, RadialVoidMask::new));
		private static final KeyDispatchDataCodec<RadialVoidMask> CODEC = KeyDispatchDataCodec.of(DATA_CODEC);

		@Override
		public double compute(FunctionContext context) {
			double x = context.blockX();
			double z = context.blockZ();
			double rawDistance = Math.sqrt(x * x + z * z);
			double transitionWidth = Math.max(1.0, transitionRadius - voidRadius);
			double boundaryInfluence = smoothStep(clampValue((rawDistance - voidRadius + transitionWidth * 2.0) / (transitionWidth * 3.0), 0.0, 1.0))
				* (1.0 - smoothStep(clampValue((rawDistance - transitionRadius - transitionWidth * 2.0) / (transitionWidth * 3.0), 0.0, 1.0)));
			double distortion = (
				0.95 * smoothNoise2d(x * 0.009 + 37.0, z * 0.009 - 91.0)
				+ 0.35 * smoothNoise2d(x * 0.027 - 19.0, z * 0.027 + 13.0)
			) * transitionWidth * 0.90 * boundaryInfluence;
			double distance = rawDistance + distortion;

			if (distance <= voidRadius) {
				return insideValue;
			}
			if (distance >= transitionRadius) {
				return outsideValue;
			}

			double t = (distance - voidRadius) / Math.max(1.0, transitionRadius - voidRadius);
			double smoothT = t * t * (3.0 - 2.0 * t);
			return insideValue + (outsideValue - insideValue) * smoothT;
		}

		@Override
		public double minValue() {
			return Math.min(insideValue, outsideValue);
		}

		@Override
		public double maxValue() {
			return Math.max(insideValue, outsideValue);
		}

		@Override
		public KeyDispatchDataCodec<? extends DensityFunction> codec() {
			return CODEC;
		}
	}

	public record IslandSpawnDensity(
		int radius,
		int surfaceY,
		int verticalRadius,
		double centerValue,
		double peakValue,
		double outsideValue
	) implements DensityFunction.SimpleFunction {
		public static final MapCodec<IslandSpawnDensity> DATA_CODEC = RecordCodecBuilder.mapCodec(instance -> instance.group(
			Codec.INT.fieldOf("radius").forGetter(IslandSpawnDensity::radius),
			Codec.INT.fieldOf("surface_y").forGetter(IslandSpawnDensity::surfaceY),
			Codec.INT.fieldOf("vertical_radius").forGetter(IslandSpawnDensity::verticalRadius),
			Codec.DOUBLE.fieldOf("center_value").forGetter(IslandSpawnDensity::centerValue),
			Codec.DOUBLE.fieldOf("peak_value").forGetter(IslandSpawnDensity::peakValue),
			Codec.DOUBLE.fieldOf("outside_value").forGetter(IslandSpawnDensity::outsideValue)
		).apply(instance, IslandSpawnDensity::new));
		private static final KeyDispatchDataCodec<IslandSpawnDensity> CODEC = KeyDispatchDataCodec.of(DATA_CODEC);

		@Override
		public double compute(FunctionContext context) {
			double x = context.blockX();
			double y = context.blockY();
			double z = context.blockZ();
			double distance = Math.sqrt(x * x + z * z);
			double broadWarp = 0.23 * smoothNoise2d(x * 0.010 + 11.0, z * 0.010 - 23.0)
				+ 0.10 * smoothNoise2d(x * 0.026 - 47.0, z * 0.026 + 29.0);
			double localWarp = 0.035 * smoothNoise2d(x * 0.055 + 71.0, z * 0.055 - 17.0);
			double warpedDistance = distance / Math.max(0.55, 1.0 + broadWarp);
			double t = Math.min(1.0, warpedDistance / Math.max(1.0, radius) + localWarp);
			double horizontalStrength = curveStrength(t);
			double mainRadius = radius * (0.095 + 0.024 * broadWarp);
			double mainSoftness = radius * (0.058 + 0.018 * clampValue(broadWarp + 0.5, 0.0, 1.0));
			double centerAnchor = 1.0 - smoothStep(clampValue((distance - mainRadius) / mainSoftness, 0.0, 1.0));

			double layerShift = 30.0 * smoothNoise2d(x * 0.011 - 31.0, z * 0.011 + 43.0)
				+ 13.0 * smoothNoise2d(x * 0.033 + 9.0, z * 0.033 - 67.0);
			double localVerticalRadius = verticalRadius * (
				1.0 + 0.30 * smoothNoise2d(x * 0.009 + 101.0, z * 0.009 - 11.0)
					+ 0.12 * smoothNoise3d(x * 0.018 - 13.0, y * 0.024 + 47.0, z * 0.018 + 29.0)
			);
			double verticalT = Math.min(1.0, Math.abs(y - surfaceY - layerShift) / Math.max(1.0, localVerticalRadius));
			double broadVerticalEnvelope = 1.0 - smoothStep(clampValue((verticalT - 0.10) / 0.88, 0.0, 1.0));
			double lobeNoise = 0.68 * smoothNoise3d(x * 0.014 + 5.0, y * 0.030 - 31.0, z * 0.014 + 17.0)
				+ 0.42 * smoothNoise3d(x * 0.036 - 61.0, y * 0.062 + 11.0, z * 0.036 - 23.0)
				+ 0.22 * smoothNoise3d(x * 0.070 + 83.0, y * 0.095 - 7.0, z * 0.070 + 41.0);
			double verticalStrength = clampValue(broadVerticalEnvelope * (0.50 + 0.56 * lobeNoise), 0.0, 1.0);
			double coreVerticalAnchor = 1.0 - smoothStep(clampValue((verticalT - 0.02) / 0.58, 0.0, 1.0));
			double coreTexture = clampValue(
				0.76
					+ 0.24 * smoothNoise3d(x * 0.018 + 113.0, y * 0.022 - 71.0, z * 0.018 - 37.0)
					+ 0.16 * smoothNoise3d(x * 0.044 - 17.0, y * 0.052 + 89.0, z * 0.044 + 101.0),
				0.42,
				1.10
			);
			double centralMass = centerAnchor * coreVerticalAnchor * coreTexture;

			double strength = Math.max(centralMass, horizontalStrength * verticalStrength);
			double value = lerp(outsideValue, peakValue, strength);
			value = lerp(value, centerValue, centerAnchor * coreVerticalAnchor);

			return clampValue(value, minValue(), maxValue());
		}

		@Override
		public double minValue() {
			return Math.min(centerValue, Math.min(peakValue, outsideValue));
		}

		@Override
		public double maxValue() {
			return Math.max(centerValue, Math.max(peakValue, outsideValue));
		}

		@Override
		public KeyDispatchDataCodec<? extends DensityFunction> codec() {
			return CODEC;
		}
	}

	private static double curveStrength(double t) {
		double brightness = sampleSpawnDensityReference(t);
		double invertedReference = clampValue((0.56 - brightness) / 0.56, 0.0, 1.0);
		double outerFade = 1.0 - smoothStep(clampValue((t - 0.78) / 0.22, 0.0, 1.0));
		return invertedReference * outerFade;
	}

	private static double sampleSpawnDensityReference(double t) {
		double[] positions = {0.00, 0.03, 0.12, 0.25, 0.38, 0.50, 0.63, 0.75, 0.88, 1.00};
		double[] values = {0.08, 0.09, 0.16, 0.33, 0.53, 0.55, 0.43, 0.30, 0.18, 0.00};

		for (int i = 1; i < positions.length; i++) {
			if (t <= positions[i]) {
				double local = (t - positions[i - 1]) / (positions[i] - positions[i - 1]);
				return lerp(values[i - 1], values[i], smoothStep(clampValue(local, 0.0, 1.0)));
			}
		}

		return values[values.length - 1];
	}

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

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

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

	private static double smoothNoise2d(double x, double z) {
		return smoothNoise3d(x, 0.0, z);
	}

	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 hashToNoise(int x, int y, int z) {
		long value = 0x9E3779B97F4A7C15L;
		value ^= (long) x * 0xBF58476D1CE4E5B9L;
		value ^= (long) y * 0x94D049BB133111EBL;
		value ^= (long) z * 0xD6E8FEB86659FD93L;
		value = (value ^ (value >>> 30)) * 0xBF58476D1CE4E5B9L;
		value = (value ^ (value >>> 27)) * 0x94D049BB133111EBL;
		value = value ^ (value >>> 31);
		return ((value >>> 11) * 0x1.0p-52) - 1.0;
	}
}
