package com.fastasyncworldedit.core.math.heightmap;

import com.sk89q.worldedit.EditSession;
import com.sk89q.worldedit.MaxChangedBlocksException;
import com.sk89q.worldedit.function.mask.Mask;
import com.sk89q.worldedit.math.BlockVector3;
import com.sk89q.worldedit.math.convolution.GaussianKernel;
import com.sk89q.worldedit.math.convolution.HeightMapFilter;
import com.sk89q.worldedit.regions.CuboidRegion;
import com.sk89q.worldedit.regions.Region;
import com.sk89q.worldedit.util.Location;

import java.lang.reflect.InvocationTargetException;
import java.util.concurrent.ThreadLocalRandom;

public interface HeightMap {

    double getHeight(int x, int z);

    void setSize(int size);

    default void perform(
            EditSession session,
            Mask mask,
            BlockVector3 pos,
            int size,
            int rotationMode,
            double yscale,
            boolean smooth,
            boolean towards,
            boolean layers
    ) throws MaxChangedBlocksException {
        int[][] data = generateHeightData(session, mask, pos, size, rotationMode, yscale, smooth, towards, layers);
        applyHeightMapData(data, session, pos, size, yscale, smooth, towards, layers);
    }

    default void applyHeightMapData(
            int[][] data,
            EditSession session,
            BlockVector3 pos,
            int size,
            double yscale,
            boolean smooth,
            boolean towards,
            boolean layers
    ) throws MaxChangedBlocksException {
        BlockVector3 top = session.getMaximumPoint();
        int maxY = top.getBlockY();
        Location min = new Location(session.getWorld(), pos.subtract(size, maxY, size).toVector3());
        BlockVector3 max = pos.add(size, maxY, size);
        Region region = new CuboidRegion(session.getWorld(), min.toBlockPoint(), max);
        com.sk89q.worldedit.math.convolution.HeightMap heightMap = new com.sk89q.worldedit.math.convolution.HeightMap(
                session,
                region,
                data[0],
                layers
        );
        if (smooth) {
            try {
                HeightMapFilter filter = (HeightMapFilter) HeightMapFilter.class.getConstructors()[0].newInstance(GaussianKernel.class
                        .getConstructors()[0].newInstance(5, 1));
                int diameter = 2 * size + 1;
                data[1] = filter.filter(data[1], diameter, diameter);
            } catch (IllegalAccessException | InvocationTargetException | InstantiationException e) {
                e.printStackTrace();
            }
        }
        if (layers) {
            heightMap.applyLayers(data[1]);
        } else {
            heightMap.apply(data[1]);
        }
    }

    default int[][] generateHeightData(
            EditSession session,
            Mask mask,
            BlockVector3 pos,
            int size,
            final int rotationMode,
            double yscale,
            boolean smooth,
            boolean towards,
            final boolean layers
    ) {
        int maxY = session.getMaxY();
        int minY = session.getMinY();
        int diameter = 2 * size + 1;
        int centerX = pos.getBlockX();
        int centerZ = pos.getBlockZ();
        int centerY = pos.getBlockY();
        int[] oldData = new int[diameter * diameter];
        int[] newData = new int[oldData.length];
        if (layers) { // Pixel accuracy
            centerY <<= 3;
            maxY <<= 3;
        }
        if (towards) {
            double sizePowInv = 1d / Math.pow(size, yscale);
            int targetY = pos.getBlockY();
            int tmpY = targetY;
            for (int x = -size; x <= size; x++) {
                int xx = centerX + x;
                for (int z = -size; z <= size; z++) {
                    int index = (z + size) * diameter + (x + size);
                    int zz = centerZ + z;
                    double raise;
                    switch (rotationMode) {
                        default:
                            raise = getHeight(x, z);
                            break;
                        case 1:
                            raise = getHeight(z, x);
                            break;
                        case 2:
                            raise = getHeight(-x, -z);
                            break;
                        case 3:
                            raise = getHeight(-z, -x);
                            break;
                    }
                    int height;
                    if (layers) {
                        height = tmpY = session.getNearestSurfaceLayer(xx, zz, tmpY, minY, maxY);
                    } else {
                        height = tmpY = session.getNearestSurfaceTerrainBlock(xx, zz, tmpY, minY, maxY);
                        if (height == -1) {
                            continue;
                        }
                    }
                    oldData[index] = height;
                    if (height == minY) {
                        newData[index] = centerY;
                        continue;
                    }
                    double raisePow = Math.pow(raise, yscale);
                    int diff = targetY - height;
                    double raiseScaled = diff * (raisePow * sizePowInv);
                    double raiseScaledAbs = Math.abs(raiseScaled);
                    int random =
                            ThreadLocalRandom
                                    .current()
                                    .nextInt(maxY + 1 - minY) - minY < (int) ((Math.ceil(raiseScaledAbs) - Math.floor(
                                    raiseScaledAbs)) * (maxY + 1 - minY)) ? (diff > 0 ? 1 : -1) : 0;
                    int raiseScaledInt = (int) raiseScaled + random;
                    newData[index] = height + raiseScaledInt;
                }
            }
        } else {
            int height = pos.getBlockY();
            for (int x = -size; x <= size; x++) {
                int xx = centerX + x;
                for (int z = -size; z <= size; z++) {
                    int index = (z + size) * diameter + (x + size);
                    int zz = centerZ + z;
                    double raise;
                    switch (rotationMode) {
                        default:
                            raise = getHeight(x, z);
                            break;
                        case 1:
                            raise = getHeight(z, x);
                            break;
                        case 2:
                            raise = getHeight(-x, -z);
                            break;
                        case 3:
                            raise = getHeight(-z, -x);
                            break;
                    }
                    if (layers) {
                        height = session.getNearestSurfaceLayer(xx, zz, height, minY, maxY);
                    } else {
                        height = session.getNearestSurfaceTerrainBlock(xx, zz, height, minY, maxY);
                        if (height == minY - 1) {
                            continue;
                        }
                    }
                    oldData[index] = height;
                    if (height == minY) {
                        newData[index] = centerY;
                        continue;
                    }
                    raise = (yscale * raise);
                    int random =
                            ThreadLocalRandom
                                    .current()
                                    .nextInt(maxY + 1 - minY) - minY < (int) ((raise - (int) raise) * (maxY - minY + 1))
                                    ? 1 : 0;
                    int newHeight = height + (int) raise + random;
                    newData[index] = newHeight;
                }
            }
        }
        return new int[][]{oldData, newData};
    }

}
