package eu.mhsl.minenet.minigames.world.generator.terrain;

import de.articdive.jnoise.JNoise;
import eu.mhsl.minenet.minigames.util.GeneratorUtils;
import eu.mhsl.minenet.minigames.util.NumberUtil;
import eu.mhsl.minenet.minigames.world.BlockPallet;
import net.minestom.server.coordinate.Point;
import net.minestom.server.coordinate.Pos;
import net.minestom.server.instance.block.Block;
import net.minestom.server.instance.generator.GenerationUnit;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

import java.util.Random;
import java.util.function.Function;

public class HeightTerrainGenerator extends BaseGenerator {
    protected final Random rnd = new Random();
    private Function<Point, Integer> calculateHeight = (Point) -> 0;
    protected Function<Point, Integer> heightNoiseMultiplier = (Point) -> 16;
    protected Function<Point, Integer> calculateSeaLevel = null;

    private final JNoise base = JNoise.newBuilder()
            .fastSimplex()
            .setSeed(rnd.nextLong())
            .setFrequency(0.01)
            .build();

    private final JNoise batches = JNoise.newBuilder()
            .fastSimplex()
            .setSeed(rnd.nextLong())
            .setFrequency(0.05)
            .build();

    private final JNoise peaks = JNoise.newBuilder()
            .fastSimplex()
            .setSeed(rnd.nextLong())
            .setFrequency(0.1)
            .build();

    private final BlockPallet blockPallet = BlockPallet.STONE;

    @Override
    public void generate(@NotNull GenerationUnit unit) {
        super.generate(unit);
        GeneratorUtils.foreachXZ(unit, bottomPoint -> {
            unit.modifier().setBlock(bottomPoint, blockPallet.rnd());

            synchronized (base) {
                double heightNoise = base.getNoise(bottomPoint.x(), bottomPoint.z());
                double noiseModifier = heightNoise * heightNoiseMultiplier.apply(bottomPoint);
                double heightModifier = NumberUtil.clamp(calculateHeight.apply(bottomPoint) + noiseModifier, 1d, unit.size().y());
                if(heightModifier < 1) System.out.println("HEIGHT MODIFIER ILLEGAL");

                synchronized (batches) {
                    double batchNoise = batches.getNoise(bottomPoint.x(), bottomPoint.z());

                    unit.modifier().fill(bottomPoint, bottomPoint.add(1, heightModifier, 1), batchNoise < 0.9 ? batchNoise > 0 ? Block.GRASS_BLOCK : Block.SOUL_SAND : Block.STONE);

                    if(calculateSeaLevel != null) {
                        Point absoluteHeight = bottomPoint.add(0, heightModifier, 0);
                        int seaLevel = calculateSeaLevel.apply(bottomPoint);
                        if(absoluteHeight.y() < seaLevel) {
//                            System.out.println("HM:" + absoluteHeight.y() + " SL:" + seaLevel);
//                            System.out.println("Filling from " + bottomPoint.y() + " to " + absoluteHeight.withY(seaLevel).y());
                            unit.modifier().fill(bottomPoint.withY(v -> v+heightModifier), absoluteHeight.add(1, 0, 1).withY(seaLevel), Block.WATER);
                        }
                    }
                }

                synchronized (peaks) {
                    double peakNoise = peaks.getNoise(bottomPoint.x(), bottomPoint.z());

                    if(peakNoise > 0.97 && bottomPoint.distance(new Pos(0, 0, 0)) > (50 + 20)) {

                        Point center = bottomPoint.add(1, heightModifier, 1);
//                        unit.fork(setter -> new PeakRock(center, BlockPallet.STONE).generateStructure(setter));

//                        unit.fork(setter -> new Tree(center).generateStructure(setter));
                    }
                }
            }
        });
    }

    public void setCalculateHeight(Function<Point, Integer> calculateHeight) {
        this.calculateHeight = calculateHeight;
    }

    public void setHeightNoiseMultiplier(Function<Point, Integer> heightNoiseMultiplier) {
        this.heightNoiseMultiplier = heightNoiseMultiplier;
    }

    public void setCalculateSeaLevel(@Nullable Function<Point, Integer> calculateSeaLevel) {
        this.calculateSeaLevel = calculateSeaLevel;
    }
}
