package com.fastasyncworldedit.core.command.tool.brush;

import com.fastasyncworldedit.core.math.MutableVector3;
import com.fastasyncworldedit.core.math.random.SimplexNoise;
import com.fastasyncworldedit.core.util.MathMan;
import com.sk89q.worldedit.EditSession;
import com.sk89q.worldedit.MaxChangedBlocksException;
import com.sk89q.worldedit.command.tool.brush.Brush;
import com.sk89q.worldedit.function.pattern.Pattern;
import com.sk89q.worldedit.math.BlockVector3;
import com.sk89q.worldedit.math.Vector3;
import com.sk89q.worldedit.math.transform.AffineTransform;

import java.util.concurrent.ThreadLocalRandom;

public class BlobBrush implements Brush {

    private final double amplitude;
    private final double frequency;
    private final Vector3 radius;
    private final double sphericity;

    public BlobBrush(Vector3 radius, double frequency, double amplitude, double sphericity) {
        this.frequency = frequency;
        this.amplitude = amplitude;
        this.radius = radius;
        this.sphericity = sphericity;
    }

    @Override
    public void build(EditSession editSession, BlockVector3 position, Pattern pattern, double size) throws
            MaxChangedBlocksException {
        double seedX = ThreadLocalRandom.current().nextDouble();
        double seedY = ThreadLocalRandom.current().nextDouble();
        double seedZ = ThreadLocalRandom.current().nextDouble();

        int px = position.getBlockX();
        int py = position.getBlockY();
        int pz = position.getBlockZ();

        double distort = this.frequency / size;

        double modX = 1d / radius.getX();
        double modY = 1d / radius.getY();
        double modZ = 1d / radius.getZ();
        int radius = (int) size;
        int radiusSqr = (int) (size * size);
        int sizeInt = (int) size * 2;

        if (sphericity == 1) {
            for (int x = -sizeInt; x <= sizeInt; x++) {
                double nx = seedX + x * distort;
                double d1 = x * x * modX;
                for (int y = -sizeInt; y <= sizeInt; y++) {
                    double d2 = d1 + y * y * modY;
                    double ny = seedY + y * distort;
                    for (int z = -sizeInt; z <= sizeInt; z++) {
                        double nz = seedZ + z * distort;
                        double distance = d2 + z * z * modZ;
                        double noise = this.amplitude * SimplexNoise.noise(nx, ny, nz);
                        if (distance + distance * noise < radiusSqr) {
                            editSession.setBlock(px + x, py + y, pz + z, pattern);
                        }
                    }
                }
            }
        } else {
            AffineTransform transform = new AffineTransform()
                    .rotateX(ThreadLocalRandom.current().nextInt(360))
                    .rotateY(ThreadLocalRandom.current().nextInt(360))
                    .rotateZ(ThreadLocalRandom.current().nextInt(360));

            double manScaleX = 1.25 + seedX * 0.5;
            double manScaleY = 1.25 + seedY * 0.5;
            double manScaleZ = 1.25 + seedZ * 0.5;

            MutableVector3 mutable = new MutableVector3();
            double roughness = 1 - sphericity;
            for (int xr = -sizeInt; xr <= sizeInt; xr++) {
                for (int yr = -sizeInt; yr <= sizeInt; yr++) {
                    for (int zr = -sizeInt; zr <= sizeInt; zr++) {
                        // pt == mutable as it's a MutableVector3
                        // so it must be set each time
                        mutable.mutX(xr);
                        mutable.mutY(yr);
                        mutable.mutZ(zr);
                        Vector3 pt = transform.apply(mutable);
                        int x = MathMan.roundInt(pt.getX());
                        int y = MathMan.roundInt(pt.getY());
                        int z = MathMan.roundInt(pt.getZ());

                        double xScaled = Math.abs(x) * modX;
                        double yScaled = Math.abs(y) * modY;
                        double zScaled = Math.abs(z) * modZ;
                        double manDist = xScaled + yScaled + zScaled;
                        double distSqr = x * x * modX + z * z * modZ + y * y * modY;

                        double distance = Math.sqrt(distSqr) * sphericity + MathMan.max(
                                manDist,
                                xScaled * manScaleX,
                                yScaled * manScaleY,
                                zScaled * manScaleZ
                        ) * roughness;

                        double noise = this.amplitude * SimplexNoise.noise(
                                seedX + x * distort,
                                seedZ + z * distort,
                                seedZ + z * distort
                        );
                        if (distance + distance * noise < radius) {
                            editSession.setBlock(px + xr, py + yr, pz + zr, pattern);
                        }
                    }
                }
            }
        }
    }

}
