/*
 * Copyright (c) 2009-2023 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 java.util.logging.Logger;

/**
 * <code>ParticleDepositionHeightMap</code> creates a heightmap based on the
 * Particle Deposition algorithm based on Jason Shankel's paper from
 * "Game Programming Gems". A heightmap is created using a Molecular beam
 * epitaxy, or MBE, for depositing thin layers of atoms on a substrate.
 * We drop a sequence of particles and simulate their flow across a surface
 * of previously dropped particles. This creates a few high peaks, for further
 * realism we can define a caldera. Similar to the way volcano's form
 * islands, rock is deposited via lava, when the lava cools, it recedes
 * into the volcano, creating the caldera.
 *
 * @author Mark Powell
 * @version $Id$
 */
public class ParticleDepositionHeightMap extends AbstractHeightMap {

    private static final Logger logger = Logger.getLogger(ParticleDepositionHeightMap.class.getName());
    //Attributes.
    private int jumps;
    private int peakWalk;
    private int minParticles;
    private int maxParticles;
    private float caldera;

    /**
     * Constructor sets the attributes of the Particle Deposition
     * Height Map and then generates the map.
     *
     * @param size the size of the terrain where the area is size x size.
     * @param jumps number of areas to drop particles. Can also think
     *              of it as the number of peaks.
     * @param peakWalk determines how much to agitate the drop point
     *              during a creation of a single peak. The lower the number
     *              the more the drop point will be agitated. 1 will insure
     *              agitation every round.
     * @param minParticles defines the minimum number of particles to
     *              drop during a single jump.
     * @param maxParticles defines the maximum number of particles to
     *              drop during a single jump.
     * @param caldera defines the altitude to invert a peak. This is
     *              represented as a percentage, where 0.0 will not invert
     *              anything, and 1.0 will invert all.
     *
     * @throws Exception if any value is less than zero, and
     *              if caldera is not between 0 and 1. If minParticles is greater than
     *              max particles as well.
     */
    public ParticleDepositionHeightMap(
            int size,
            int jumps,
            int peakWalk,
            int minParticles,
            int maxParticles,
            float caldera) throws Exception {


        if (size <= 0
                || jumps < 0
                || peakWalk < 0
                || minParticles > maxParticles
                || minParticles < 0
                || maxParticles < 0) {


            throw new Exception(
                    "values must be greater than zero, "
                    + "and minParticles must be greater than maxParticles");
        }
        if (caldera < 0.0f || caldera > 1.0f) {
            throw new Exception(
                    "Caldera level must be " + "between 0 and 1");
        }


        this.size = size;
        this.jumps = jumps;
        this.peakWalk = peakWalk;
        this.minParticles = minParticles;
        this.maxParticles = maxParticles;
        this.caldera = caldera;


        load();
    }

    /**
     * <code>load</code> generates the heightfield using the Particle Deposition
     * algorithm. <code>load</code> uses the latest attributes, so a call
     * to <code>load</code> is recommended if attributes have changed using
     * the set methods.
     */
    @Override
    public boolean load() {
        int x, y;
        int calderaX, calderaY;
        int sx, sy;
        int tx, ty;
        int m;
        float calderaStartPoint;
        float cutoff;
        int dx[] = {0, 1, 0, size - 1, 1, 1, size - 1, size - 1};
        int dy[] = {1, 0, size - 1, 0, size - 1, 1, size - 1, 1};
        float[][] tempBuffer = new float[size][size];
        //map 0 unmarked, unvisited, 1 marked, unvisited, 2 marked visited.
        int[][] calderaMap = new int[size][size];
        boolean done;


        int minx, maxx;
        int miny, maxy;


        if (null != heightData) {
            unloadHeightMap();
        }


        heightData = new float[size * size];


        //create peaks.
        for (int i = 0; i < jumps; i++) {


            //pick a random point.
            x = (int) (Math.rint(Math.random() * (size - 1)));
            y = (int) (Math.rint(Math.random() * (size - 1)));


            //set the caldera point.
            calderaX = x;
            calderaY = y;


            int numberParticles =
                    (int) (Math.rint(
                    (Math.random() * (maxParticles - minParticles))
                    + minParticles));
            //drop particles.
            for (int j = 0; j < numberParticles; j++) {
                //check to see if we should agitate the drop point.
                if (peakWalk != 0 && j % peakWalk == 0) {
                    m = (int) (Math.rint(Math.random() * 7));
                    x = (x + dx[m] + size) % size;
                    y = (y + dy[m] + size) % size;
                }


                //add the particle to the point.
                tempBuffer[x][y] += 1;


                sx = x;
                sy = y;
                done = false;


                //cause the particle to "slide" down the slope and settle at
                //a low point.
                while (!done) {
                    done = true;


                    //check neighbors to see if we are higher.
                    m = (int) (Math.rint((Math.random() * 8)));
                    for (int jj = 0; jj < 8; jj++) {
                        tx = (sx + dx[(jj + m) % 8]) % (size);
                        ty = (sy + dy[(jj + m) % 8]) % (size);


                        //move to the neighbor.
                        if (tempBuffer[tx][ty] + 1.0f < tempBuffer[sx][sy]) {
                            tempBuffer[tx][ty] += 1.0f;
                            tempBuffer[sx][sy] -= 1.0f;
                            sx = tx;
                            sy = ty;
                            done = false;
                            break;
                        }
                    }
                }


                //This point is higher than the current caldera point,
                //so move the caldera here.
                if (tempBuffer[sx][sy] > tempBuffer[calderaX][calderaY]) {
                    calderaX = sx;
                    calderaY = sy;
                }
            }


            //apply the caldera.
            calderaStartPoint = tempBuffer[calderaX][calderaY];
            cutoff = calderaStartPoint * (1.0f - caldera);
            minx = calderaX;
            maxx = calderaX;
            miny = calderaY;
            maxy = calderaY;


            calderaMap[calderaX][calderaY] = 1;


            done = false;
            while (!done) {
                done = true;
                sx = minx;
                sy = miny;
                tx = maxx;
                ty = maxy;


                for (x = sx; x <= tx; x++) {
                    for (y = sy; y <= ty; y++) {


                        calderaX = (x + size) % size;
                        calderaY = (y + size) % size;


                        if (calderaMap[calderaX][calderaY] == 1) {
                            calderaMap[calderaX][calderaY] = 2;


                            if (tempBuffer[calderaX][calderaY] > cutoff
                                    && tempBuffer[calderaX][calderaY]
                                    <= calderaStartPoint) {


                                done = false;
                                tempBuffer[calderaX][calderaY] =
                                        2 * cutoff - tempBuffer[calderaX][calderaY];


                                //check the left and right neighbors
                                calderaX = (calderaX + 1) % size;
                                if (calderaMap[calderaX][calderaY] == 0) {
                                    if (x + 1 > maxx) {
                                        maxx = x + 1;
                                    }
                                    calderaMap[calderaX][calderaY] = 1;
                                }


                                calderaX = (calderaX + size - 2) % size;
                                if (calderaMap[calderaX][calderaY] == 0) {
                                    if (x - 1 < minx) {
                                        minx = x - 1;
                                    }
                                    calderaMap[calderaX][calderaY] = 1;
                                }


                                //check the upper and lower neighbors.
                                calderaX = (x + size) % size;
                                calderaY = (calderaY + 1) % size;
                                if (calderaMap[calderaX][calderaY] == 0) {
                                    if (y + 1 > maxy) {
                                        maxy = y + 1;
                                    }
                                    calderaMap[calderaX][calderaY] = 1;
                                }
                                calderaY = (calderaY + size - 2) % size;
                                if (calderaMap[calderaX][calderaY] == 0) {
                                    if (y - 1 < miny) {
                                        miny = y - 1;
                                    }
                                    calderaMap[calderaX][calderaY] = 1;
                                }
                            }
                        }
                    }
                }
            }
        }

        //transfer the new terrain into the height map.
        for (int i = 0; i < size; i++) {
            for (int j = 0; j < size; j++) {
                setHeightAtPoint(tempBuffer[i][j], j, i);
            }
        }
        erodeTerrain();
        normalizeTerrain(NORMALIZE_RANGE);

        logger.fine("Created heightmap using Particle Deposition");

        return true; // success
    }

    /**
     * <code>setJumps</code> sets the number of jumps or peaks that will
     * be created during the next call to <code>load</code>.
     * @param jumps the number of jumps to use for next load.
     * @throws Exception if jumps is less than zero.
     */
    public void setJumps(int jumps) throws Exception {
        if (jumps < 0) {
            throw new Exception("jumps must be positive");
        }
        this.jumps = jumps;
    }

    /**
     * <code>setPeakWalk</code> sets how often the jump point will be
     * agitated. The lower the peakWalk, the more often the point will
     * be agitated.
     *
     * @param peakWalk the amount to agitate the jump point.
     * @throws Exception if peakWalk is negative or zero.
     */
    public void setPeakWalk(int peakWalk) throws Exception {
        if (peakWalk <= 0) {
            throw new Exception(
                    "peakWalk must be greater than " + "zero");
        }
        this.peakWalk = peakWalk;
    }

    /**
     * <code>setCaldera</code> sets the level at which a peak will be
     * inverted.
     *
     * @param caldera the level at which a peak will be inverted. This must be
     *              between 0 and 1, as it is represented as a percentage.
     * @throws Exception if caldera is not between 0 and 1.
     */
    public void setCaldera(float caldera) throws Exception {
        if (caldera < 0.0f || caldera > 1.0f) {
            throw new Exception(
                    "Caldera level must be " + "between 0 and 1");
        }
        this.caldera = caldera;
    }

    /**
     * <code>setMaxParticles</code> sets the maximum number of particles
     * for a single jump.
     * @param maxParticles the maximum number of particles for a single jump.
     */
    public void setMaxParticles(int maxParticles) {
        this.maxParticles = maxParticles;
    }

    /**
     * <code>setMinParticles</code> sets the minimum number of particles
     * for a single jump.
     * @param minParticles the minimum number of particles for a single jump.
     * @throws Exception if minParticles are greater than
     *              the current maxParticles;
     */
    public void setMinParticles(int minParticles) throws Exception {
        if (minParticles > maxParticles) {
            throw new Exception(
                    "minParticles must be less " + "than the current maxParticles");
        }
        this.minParticles = minParticles;
    }
}
