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

/**
 * <code>FluidSimHeightMap</code> generates a height map based using some
 * sort of fluid simulation. The heightmap is treated as a highly viscous and
 * rubbery fluid enabling to fine tune the generated heightmap using a number
 * of parameters.
 *
 * @author Frederik Boelthoff
 * @see <a href="http://www.gamedev.net/reference/articles/article2001.asp">Terrain Generation Using Fluid Simulation</a>
 * @version $Id$
 *
 */
public class FluidSimHeightMap extends AbstractHeightMap {

    private static final Logger logger = Logger.getLogger(FluidSimHeightMap.class.getName());
    private float waveSpeed = 100.0f;  // speed at which the waves travel
    private float timeStep = 0.033f;  // constant time-step between each iteration
    private float nodeDistance = 10.0f;   // distance between each node of the surface
    private float viscosity = 100.0f; // viscosity of the fluid
    private int iterations;    // number of iterations
    private float minInitialHeight = -500; // min initial height
    private float maxInitialHeight = 500; // max initial height
    private long seed; // the seed for the random number generator
    float coefA, coefB, coefC; // pre-computed coefficients in the fluid equation

    /**
     * Constructor sets the attributes of the hill system and generates the
     * height map. It gets passed a number of tweakable parameters which
     * fine-tune the outcome.
     *
     * @param size
     *            size the size of the terrain to be generated
     * @param iterations
     *            the number of iterations to do
     * @param minInitialHeight
     *                        the minimum initial height of a terrain value
     * @param maxInitialHeight
     *                        the maximum initial height of a terrain value
     * @param viscosity
     *                        the viscosity of the fluid
     * @param waveSpeed
     *                        the speed at which the waves travel
     * @param timestep
     *                        the constant time-step between each iteration
     * @param nodeDistance
     *                        the distance between each node of the heightmap
     * @param seed
     *            the seed to generate the same heightmap again
     * @throws JmeException
     *             if size of the terrain is not greater that zero, or number of
     *             iterations is not greater that zero, or the minimum initial height
     *             is greater than the maximum (or the other way around)
     */
    public FluidSimHeightMap(int size, int iterations, float minInitialHeight, float maxInitialHeight, float viscosity, float waveSpeed, float timestep, float nodeDistance, long seed) throws Exception {
        if (size <= 0 || iterations <= 0 || minInitialHeight >= maxInitialHeight) {
            throw new Exception(
                    "Either size of the terrain is not greater that zero, "
                    + "or number of iterations is not greater that zero, "
                    + "or minimum height greater or equal as the maximum, "
                    + "or maximum height smaller or equal as the minimum.");
        }

        this.size = size;
        this.seed = seed;
        this.iterations = iterations;
        this.minInitialHeight = minInitialHeight;
        this.maxInitialHeight = maxInitialHeight;
        this.viscosity = viscosity;
        this.waveSpeed = waveSpeed;
        this.timeStep = timestep;
        this.nodeDistance = nodeDistance;

        load();
    }

    /**
     * Constructor sets the attributes of the hill system and generates the
     * height map.
     *
     * @param size
     *            size the size of the terrain to be generated
     * @param iterations
     *            the number of iterations to do
     * @throws JmeException
     *             if size of the terrain is not greater that zero, or number of
     *             iterations is not greater that zero
     */
    public FluidSimHeightMap(int size, int iterations) throws Exception {
        if (size <= 0 || iterations <= 0) {
            throw new Exception(
                    "Either size of the terrain is not greater that zero, "
                    + "or number of iterations is not greater that zero");
        }

        this.size = size;
        this.iterations = iterations;

        load();
    }


    /*
     * Generates a heightmap using fluid simulation and the attributes set by
     * the constructor or the setters.
     */
    public boolean load() {
        // Clean up data if needed.
        if (null != heightData) {
            unloadHeightMap();
        }

        heightData = new float[size * size];
        float[][] tempBuffer = new float[2][size * size];
        Random random = new Random(seed);

        // pre-compute the coefficients in the fluid equation
        coefA = (4 - (8 * waveSpeed * waveSpeed * timeStep * timeStep) / (nodeDistance * nodeDistance)) / (viscosity * timeStep + 2);
        coefB = (viscosity * timeStep - 2) / (viscosity * timeStep + 2);
        coefC = ((2 * waveSpeed * waveSpeed * timeStep * timeStep) / (nodeDistance * nodeDistance)) / (viscosity * timeStep + 2);

        // initialize the heightmaps to random values except for the edges
        for (int i = 0; i < size; i++) {
            for (int j = 0; j < size; j++) {
                tempBuffer[0][j + i * size] = tempBuffer[1][j + i * size] = randomRange(random, minInitialHeight, maxInitialHeight);
            }
        }

        int curBuf = 0;
        int ind;

        float[] oldBuffer;
        float[] newBuffer;

        // Iterate over the heightmap, applying the fluid simulation equation.
        // Although it requires knowledge of the two previous timesteps, it only
        // accesses one pixel of the k-1 timestep, so using a simple trick we only
        // need to store the heightmap twice, not three times, and we can avoid
        // copying data every iteration.
        for (int i = 0; i < iterations; i++) {
            oldBuffer = tempBuffer[1 - curBuf];
            newBuffer = tempBuffer[curBuf];

            for (int y = 0; y < size; y++) {
                for (int x = 0; x < size; x++) {
                    ind = x + y * size;
                    float neighborsValue = 0;
                    int neighbors = 0;

                    if (x > 0) {
                        neighborsValue += newBuffer[ind - 1];
                        neighbors++;
                    }
                    if (x < size - 1) {
                        neighborsValue += newBuffer[ind + 1];
                        neighbors++;
                    }
                    if (y > 0) {
                        neighborsValue += newBuffer[ind - size];
                        neighbors++;
                    }
                    if (y < size - 1) {
                        neighborsValue += newBuffer[ind + size];
                        neighbors++;
                    }
                    if (neighbors != 4) {
                        neighborsValue *= 4 / neighbors;
                    }
                    oldBuffer[ind] = coefA * newBuffer[ind] + coefB
                            * oldBuffer[ind] + coefC * (neighborsValue);
                }
            }

            curBuf = 1 - curBuf;
        }

        // put the normalized heightmap into the range [0...255] and into the heightmap
        for (int y = 0; y < size; y++) {
            for (int x = 0; x < size; x++) {
                heightData[x + y * size] = (float) (tempBuffer[curBuf][x + y * size]);
            }
        }
        normalizeTerrain(NORMALIZE_RANGE);

        logger.info("Created Heightmap using fluid simulation");

        return true;
    }

    private float randomRange(Random random, float min, float max) {
        return (random.nextFloat() * (max - min)) + min;
    }

    /**
     * Sets the number of times the fluid simulation should be iterated over
     * the heightmap. The more often this is, the less features (hills, etc)
     * the terrain will have, and the smoother it will be.
     *
     * @param iterations
     *            the number of iterations to do
     * @throws JmeException
     *             if iterations if not greater than zero
     */
    public void setIterations(int iterations) throws Exception {
        if (iterations <= 0) {
            throw new Exception(
                    "Number of iterations is not greater than zero");
        }
        this.iterations = iterations;
    }

    /**
     * Sets the maximum initial height of the terrain.
     *
     * @param maxInitialHeight
     *                        the maximum initial height
     * @see #setMinInitialHeight(int)
     */
    public void setMaxInitialHeight(float maxInitialHeight) {
        this.maxInitialHeight = maxInitialHeight;
    }

    /**
     * Sets the minimum initial height of the terrain.
     *
     * @param minInitialHeight
     *                        the minimum initial height
     * @see #setMaxInitialHeight(int)
     */
    public void setMinInitialHeight(float minInitialHeight) {
        this.minInitialHeight = minInitialHeight;
    }

    /**
     * Sets the distance between each node of the heightmap.
     *
     * @param nodeDistance
     *                       the distance between each node
     */
    public void setNodeDistance(float nodeDistance) {
        this.nodeDistance = nodeDistance;
    }

    /**
     * Sets the time-speed between each iteration of the fluid
     * simulation algortithm.
     *
     * @param timeStep
     *                       the time-step between each iteration
     */
    public void setTimeStep(float timeStep) {
        this.timeStep = timeStep;
    }

    /**
     * Sets the viscosity of the simulated fuid.
     *
     * @param viscosity
     *                      the viscosity of the fluid
     */
    public void setViscosity(float viscosity) {
        this.viscosity = viscosity;
    }

    /**
     * Sets the speed at which the waves trave.
     *
     * @param waveSpeed
     *                      the speed at which the waves travel
     */
    public void setWaveSpeed(float waveSpeed) {
        this.waveSpeed = waveSpeed;
    }
}
