/*
 * Copyright (c) 2009-2021 jMonkeyEngine
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are
 * met:
 *
 * * Redistributions of source code must retain the above copyright
 *   notice, this list of conditions and the following disclaimer.
 *
 * * Redistributions in binary form must reproduce the above copyright
 *   notice, this list of conditions and the following disclaimer in the
 *   documentation and/or other materials provided with the distribution.
 *
 * * Neither the name of 'jMonkeyEngine' nor the names of its contributors
 *   may be used to endorse or promote products derived from this software
 *   without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
 * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
 * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
 * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
 * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
 * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */
package com.jme3.terrain.heightmap;

import com.jme3.math.FastMath;
import java.util.Random;
import java.util.logging.Level;
import java.util.logging.Logger;

/**
 * <code>MidpointDisplacementHeightMap</code> generates a heightmap based on
 * the midpoint displacement algorithm. See Constructor javadoc for more info.
 * @author cghislai
 */
public class MidpointDisplacementHeightMap extends AbstractHeightMap {

    private static final Logger logger = Logger.getLogger(MidpointDisplacementHeightMap.class.getName());
    private float range; // The offset in which randomness will be added
    private float persistence; // How the random offset evolves with increasing passes
    private long seed; // seed for random number generator

    /**
     * The constructor generates the heightmap. After the first 4 corners are
     * randomly given a height, the center height will be set to the average of
     * the 4 corners plus a random value. Then other passes fill
     * the heightmap by the same principle.
     * The random value is generated between the values <code>-range</code>
     * and <code>range</code>. The <code>range</code> parameter is multiplied by
     * the <code>persistence</code> parameter each pass to smoothen close cell heights.
     * Extends this class and override the getOffset function for more control of
     * the randomness (you can use the coordinates and/or the computed average height
     * to influence the random amount added.
     *
     * @param size
     *          The size of the heightmap, must be 2^N+1
     * @param range
     *          The range in which randomness will be added. A value of 1 will
     *          allow -1 to 1 value changes.
     * @param persistence
     *          The factor by which the range will evolve at each iteration.
     *          A value of 0.5f will halve the range at each iteration and is
     *          typically a good choice
     * @param seed
     *          A seed to feed the random number generator.
     * @throws IllegalArgumentException if size is not a power of two plus one.
     */
    public MidpointDisplacementHeightMap(int size, float range, float persistence, long seed) {
        if (size < 0 || !FastMath.isPowerOfTwo(size - 1)) {
            throw new IllegalArgumentException("The size is negative or not of the form 2^N +1"
                    + " (a power of two plus one)");
        }
        this.size = size;
        this.range = range;
        this.persistence = persistence;
        this.seed = seed;
        load();
    }

    /**
     * The constructor generates the heightmap. After the first 4 corners are
     * randomly given a height, the center height will be set to the average of
     * the 4 corners plus a random value. Then other passes fill
     * the heightmap by the same principle.
     * The random value is generated between the values <code>-range</code>
     * and <code>range</code>. The <code>range</code> parameter is multiplied by
     * the <code>persistence</code> parameter each pass to smoothen close cell heights.
     * @param size
     *          The size of the heightmap, must be 2^N+1
     * @param range
     *          The range in which randomness will be added. A value of 1 will
     *          allow -1 to 1 value changes. 
     * @param persistence
     *          The factor by which the range will evolve at each iteration.
     *          A value of 0.5f will halve the range at each iteration and is
     *          typically a good choice
     * @throws IllegalArgumentException if size is not a power of two plus one.
     */
    public MidpointDisplacementHeightMap(int size, float range, float persistence) throws Exception {
        this(size, range, persistence, new Random().nextLong());
    }

    /**
     * Generate the heightmap.
     * @return true
     */
    @Override
    public boolean load() {
        // clean up data if needed.
        if (null != heightData) {
            unloadHeightMap();
        }
        heightData = new float[size * size];
        float[][] tempBuffer = new float[size][size];
        Random random = new Random(seed);

        tempBuffer[0][0] = random.nextFloat();
        tempBuffer[0][size - 1] = random.nextFloat();
        tempBuffer[size - 1][0] = random.nextFloat();
        tempBuffer[size - 1][size - 1] = random.nextFloat();

        float offsetRange = range;
        int stepSize = size - 1;
        while (stepSize > 1) {
            int[] nextCoords = {0, 0};
            while (nextCoords != null) {
                nextCoords = doSquareStep(tempBuffer, nextCoords, stepSize, offsetRange, random);
            }
            nextCoords = new int[]{0, 0};
            while (nextCoords != null) {
                nextCoords = doDiamondStep(tempBuffer, nextCoords, stepSize, offsetRange, random);
            }
            stepSize /= 2;
            offsetRange *= persistence;
        }

        for (int i = 0; i < size; i++) {
            for (int j = 0; j < size; j++) {
                setHeightAtPoint(tempBuffer[i][j], j, i);
            }
        }

        normalizeTerrain(NORMALIZE_RANGE);

        logger.log(Level.FINE, "Midpoint displacement heightmap generated");
        return true;
    }

    /**
     * Will fill the value at (coords[0]+stepSize/2, coords[1]+stepSize/2) with
     * the average from the corners of the square with top, left corner at (coords[0],coords[1])
     * and width of stepSize.
     * @param tempBuffer the temporary heightmap
     * @param coords an int array of length 2 with the x coord in position 0
     * @param stepSize the size of the square
     * @param offsetRange the offset range within a random value is picked and added to the average
     * @param random the random generator
     * @return a new array or null
     */
    protected int[] doSquareStep(float[][] tempBuffer, int[] coords, int stepSize, float offsetRange, Random random) {
        float cornerAverage = 0;
        int x = coords[0];
        int y = coords[1];
        cornerAverage += tempBuffer[x][y];
        cornerAverage += tempBuffer[x + stepSize][y];
        cornerAverage += tempBuffer[x + stepSize][y + stepSize];
        cornerAverage += tempBuffer[x][y + stepSize];
        cornerAverage /= 4;
        float offset = getOffset(random, offsetRange, coords, cornerAverage);
        tempBuffer[x + stepSize / 2][y + stepSize / 2] = cornerAverage + offset;

        // Only get to next square if the center is still in map
        if (x + stepSize * 3 / 2 < size) {
            return new int[]{x + stepSize, y};
        }
        if (y + stepSize * 3 / 2 < size) {
            return new int[]{0, y + stepSize};
        }
        return null;
    }

    /**
     * Will fill the cell at (x+stepSize/2, y) with the average of the 4 corners
     * of the diamond centered on that point with width and height of stepSize.
     * @param tempBuffer
     * @param coords
     * @param stepSize
     * @param offsetRange
     * @param random
     * @return a new array or null
     */
    protected int[] doDiamondStep(float[][] tempBuffer, int[] coords, int stepSize, float offsetRange, Random random) {
        int cornerNbr = 0;
        float cornerAverage = 0;
        int x = coords[0];
        int y = coords[1];
        int[] dxs = new int[]{0, stepSize / 2, stepSize, stepSize / 2};
        int[] dys = new int[]{0, -stepSize / 2, 0, stepSize / 2};

        for (int d = 0; d < 4; d++) {
            int i = x + dxs[d];
            if (i < 0 || i > size - 1) {
                continue;
            }
            int j = y + dys[d];
            if (j < 0 || j > size - 1) {
                continue;
            }
            cornerAverage += tempBuffer[i][j];
            cornerNbr++;
        }
        cornerAverage /= cornerNbr;
        float offset = getOffset(random, offsetRange, coords, cornerAverage);
        tempBuffer[x + stepSize / 2][y] = cornerAverage + offset;

        if (x + stepSize * 3 / 2 < size) {
            return new int[]{x + stepSize, y};
        }
        if (y + stepSize / 2 < size) {
            if (x + stepSize == size - 1) {
                return new int[]{-stepSize / 2, y + stepSize / 2};
            } else {
                return new int[]{0, y + stepSize / 2};
            }
        }
        return null;
    }

    /**
     * Generate a random value to add  to the computed average
     * @param random the random generator
     * @param offsetRange
     * @param coords
     * @param average
     * @return A semi-random value within offsetRange
     */
    protected float getOffset(Random random, float offsetRange, int[] coords, float average) {
        return 2 * (random.nextFloat() - 0.5F) * offsetRange;
    }

    public float getPersistence() {
        return persistence;
    }

    public void setPersistence(float persistence) {
        this.persistence = persistence;
    }

    public float getRange() {
        return range;
    }

    public void setRange(float range) {
        this.range = range;
    }

    public long getSeed() {
        return seed;
    }

    public void setSeed(long seed) {
        this.seed = seed;
    }
}
