/*
 * Copyright (c) 2009-2012 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;

/**
 * Creates an heightmap based on the fault algorithm. Each iteration, a random line
 * crossing the map is generated. On one side height values are raised, on the other side
 * lowered.
 * @author cghislai
 */
public class FaultHeightMap extends AbstractHeightMap {

    private static final Logger logger = Logger.getLogger(FaultHeightMap.class.getName());
    /**
     * Values on one side are lowered, on the other side increased,
     * creating a step at the fault line
     */
    public static final int FAULTTYPE_STEP = 0;
    /**
     * Values on one side are lowered, then increase lineary while crossing
     * the fault line to the other side. The fault line will be a inclined
     * plane
     */
    public static final int FAULTTYPE_LINEAR = 1;
    /**
     * Values are lowered on one side, increased on the other, creating a
     * cosine curve on the fault line
     */
    public static final int FAULTTYPE_COSINE = 2;
    /**
     * Value are lowered on both side, but increased on the fault line
     * creating a smooth ridge on the fault line.
     */
    public static final int FAULTTYPE_SINE = 3;
    /**
     * A linear fault is created
     */
    public static final int FAULTSHAPE_LINE = 10;
    /**
     * A circular fault is created.
     */
    public static final int FAULTSHAPE_CIRCLE = 11;
    private long seed; // A seed to feed the random
    private int iterations; // iterations to perform
    private float minFaultHeight; // the height modification applied
    private float maxFaultHeight; // the height modification applied
    private float minRange; // The range for linear and trigo faults
    private float maxRange; // The range for linear and trigo faults
    private float minRadius; // radii for circular fault
    private float maxRadius;
    private int faultType; // The type of fault
    private int faultShape; // The type of fault

    /**
     * Constructor creates the fault. For faulttype other than STEP, a range can
     * be provided. For faultshape circle, min and max radii can be provided.
     * Don't forget to reload the map if you have set parameters after the constructor
     * call.
     * @param size The size of the heightmap
     * @param iterations Iterations to perform
     * @param faultType Type of fault
     * @param faultShape Shape of the fault -line or circle
     * @param minFaultHeight Height modified on each side
     * @param maxFaultHeight Height modified on each side
     * @param seed A seed to feed the Random generator
     * @see setFaultRange, setMinRadius, setMaxRadius
     */
    public FaultHeightMap(int size, int iterations, int faultType, int faultShape, float minFaultHeight, float maxFaultHeight, long seed) throws Exception {
        if (size < 0 || iterations < 0) {
            throw new Exception("Size and iterations must be greater than 0!");
        }
        this.size = size;
        this.iterations = iterations;
        this.faultType = faultType;
        this.faultShape = faultShape;
        this.minFaultHeight = minFaultHeight;
        this.maxFaultHeight = maxFaultHeight;
        this.seed = seed;
        this.minRange = minFaultHeight;
        this.maxRange = maxFaultHeight;
        this.minRadius = size / 10;
        this.maxRadius = size / 4;
        load();
    }

    /**
     * Create an heightmap with linear step faults.
     * @param size size of heightmap
     * @param iterations number of iterations
     * @param minFaultHeight Height modified on each side
     * @param maxFaultHeight Height modified on each side
     */
    public FaultHeightMap(int size, int iterations, float minFaultHeight, float maxFaultHeight) throws Exception {
        this(size, iterations, FAULTTYPE_STEP, FAULTSHAPE_LINE, minFaultHeight, maxFaultHeight, new Random().nextLong());
    }

    @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);

        for (int i = 0; i < iterations; i++) {
            addFault(tempBuffer, random);
        }

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

        normalizeTerrain(NORMALIZE_RANGE);

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

    protected void addFault(float[][] tempBuffer, Random random) {
        float faultHeight = minFaultHeight + random.nextFloat() * (maxFaultHeight - minFaultHeight);
        float range = minRange + random.nextFloat() * (maxRange - minRange);
        switch (faultShape) {
            case FAULTSHAPE_LINE:
                addLineFault(tempBuffer, random, faultHeight, range);
                break;
            case FAULTSHAPE_CIRCLE:
                addCircleFault(tempBuffer, random, faultHeight, range);
                break;
        }
    }

    protected void addLineFault(float[][] tempBuffer, Random random, float faultHeight, float range) {
        int x1 = random.nextInt(size);
        int x2 = random.nextInt(size);
        int y1 = random.nextInt(size);
        int y2 = random.nextInt(size);


        for (int i = 0; i < size; i++) {
            for (int j = 0; j < size; j++) {
                float dist = ((x2 - x1) * (j - y1) - (y2 - y1) * (i - x1))
                        / (FastMath.sqrt(FastMath.sqr(x2 - x1) + FastMath.sqr(y2 - y1)));
                tempBuffer[i][j] += calcHeight(dist, random, faultHeight, range);
            }
        }
    }

    protected void addCircleFault(float[][] tempBuffer, Random random, float faultHeight, float range) {
        float radius = random.nextFloat() * (maxRadius - minRadius) + minRadius;
        int intRadius = (int) FastMath.floor(radius);
        // Allox circle center to be out of map if not by more than radius.
        // Unlucky cases will put them in the far corner, with the circle
        // entirely outside heightmap
        int x = random.nextInt(size + 2 * intRadius) - intRadius;
        int y = random.nextInt(size + 2 * intRadius) - intRadius;

        for (int i = 0; i < size; i++) {
            for (int j = 0; j < size; j++) {
                float dist;
                if (i != x || j != y) {
                    int dx = i - x;
                    int dy = j - y;
                    float dmag = FastMath.sqrt(FastMath.sqr(dx) + FastMath.sqr(dy));
                    float rx = x + dx / dmag * radius;
                    float ry = y + dy / dmag * radius;
                    dist = FastMath.sign(dmag - radius)
                        * FastMath.sqrt(FastMath.sqr(i - rx) + FastMath.sqr(j - ry));
                } else {
                    dist = 0;
                }
                tempBuffer[i][j] += calcHeight(dist, random, faultHeight, range);
            }
        }
    }

    protected float calcHeight(float dist, Random random, float faultHeight, float range) {
        switch (faultType) {
            case FAULTTYPE_STEP: {
                return FastMath.sign(dist) * faultHeight;
            }
            case FAULTTYPE_LINEAR: {
                if (FastMath.abs(dist) > range) {
                    return FastMath.sign(dist) * faultHeight;
                }
                float f = FastMath.abs(dist) / range;
                return FastMath.sign(dist) * faultHeight * f;
            }
            case FAULTTYPE_SINE: {
                if (FastMath.abs(dist) > range) {
                    return -faultHeight;
                }
                float f = dist / range;
                // We want -1 at f=-1 and f=1; 1 at f=0
                return FastMath.sin((1 + 2 * f) * FastMath.PI / 2) * faultHeight;
            }
            case FAULTTYPE_COSINE: {
                if (FastMath.abs(dist) > range) {
                    return -FastMath.sign(dist) * faultHeight;
                }
                float f = dist / range;
                float val =  FastMath.cos((1 + f) * FastMath.PI / 2) * faultHeight;
                return val;
            }
        }
        //shoudn't go here
        throw new RuntimeException("Code needs update to switch allcases");
    }

    public int getFaultShape() {
        return faultShape;
    }

    public void setFaultShape(int faultShape) {
        this.faultShape = faultShape;
    }

    public int getFaultType() {
        return faultType;
    }

    public void setFaultType(int faultType) {
        this.faultType = faultType;
    }

    public int getIterations() {
        return iterations;
    }

    public void setIterations(int iterations) {
        this.iterations = iterations;
    }

    public float getMaxFaultHeight() {
        return maxFaultHeight;
    }

    public void setMaxFaultHeight(float maxFaultHeight) {
        this.maxFaultHeight = maxFaultHeight;
    }

    public float getMaxRadius() {
        return maxRadius;
    }

    public void setMaxRadius(float maxRadius) {
        this.maxRadius = maxRadius;
    }

    public float getMaxRange() {
        return maxRange;
    }

    public void setMaxRange(float maxRange) {
        this.maxRange = maxRange;
    }

    public float getMinFaultHeight() {
        return minFaultHeight;
    }

    public void setMinFaultHeight(float minFaultHeight) {
        this.minFaultHeight = minFaultHeight;
    }

    public float getMinRadius() {
        return minRadius;
    }

    public void setMinRadius(float minRadius) {
        this.minRadius = minRadius;
    }

    public float getMinRange() {
        return minRange;
    }

    public void setMinRange(float minRange) {
        this.minRange = minRange;
    }

    public long getSeed() {
        return seed;
    }

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