/*
 * 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.
 */
// $Id: Sphere.java 4163 2009-03-25 01:14:55Z matt.yellen $
package com.jme3.scene.shape;

import com.jme3.export.InputCapsule;
import com.jme3.export.JmeExporter;
import com.jme3.export.JmeImporter;
import com.jme3.export.OutputCapsule;
import com.jme3.math.FastMath;
import com.jme3.math.Vector3f;
import com.jme3.scene.Mesh;
import com.jme3.scene.VertexBuffer.Type;
import com.jme3.util.BufferUtils;
import com.jme3.util.TempVars;
import java.io.IOException;
import java.nio.FloatBuffer;
import java.nio.ShortBuffer;

/**
 * <code>Sphere</code> represents a 3D object with all points equidistance
 * from a center point.
 * 
 * @author Joshua Slack
 * @version $Revision: 4163 $, $Date: 2009-03-24 21:14:55 -0400 (Tue, 24 Mar 2009) $
 */
public class Sphere extends Mesh {

    public enum TextureMode {

        /** 
         * Wrap texture radially and along z-axis 
         */
        Original,
        /** 
         * Wrap texure radially, but spherically project along z-axis 
         */
        Projected,
        /** 
         * Apply texture to each pole.  Eliminates polar distortion,
         * but mirrors the texture across the equator 
         */
        Polar
    }
    protected int vertCount;
    protected int triCount;
    protected int zSamples;
    protected int radialSamples;
    protected boolean useEvenSlices;
    protected boolean interior;
    /** the distance from the center point each point falls on */
    public float radius;
    protected TextureMode textureMode = TextureMode.Original;

    /**
     * Serialization only. Do not use.
     */
    public Sphere() {
    }

    /**
     * Constructs a sphere. All geometry data buffers are updated automatically.
     * Both zSamples and radialSamples increase the quality of the generated
     * sphere.
     * 
     * @param zSamples
     *            The number of samples along the Z.
     * @param radialSamples
     *            The number of samples along the radial.
     * @param radius
     *            The radius of the sphere.
     */
    public Sphere(int zSamples, int radialSamples, float radius) {
        this(zSamples, radialSamples, radius, false, false);
    }

    /**
     * Constructs a sphere. Additional arg to evenly space latitudinal slices
     * 
     * @param zSamples
     *            The number of samples along the Z.
     * @param radialSamples
     *            The number of samples along the radial.
     * @param radius
     *            The radius of the sphere.
     * @param useEvenSlices
     *            Slice sphere evenly along the Z axis
     * @param interior
     *            Not yet documented
     */
    public Sphere(int zSamples, int radialSamples, float radius, boolean useEvenSlices, boolean interior) {
        updateGeometry(zSamples, radialSamples, radius, useEvenSlices, interior);
    }

    public int getRadialSamples() {
        return radialSamples;
    }

    public float getRadius() {
        return radius;
    }

    /**
     * @return Returns the textureMode.
     */
    public TextureMode getTextureMode() {
        return textureMode;
    }

    public int getZSamples() {
        return zSamples;
    }

    /**
     * builds the vertices based on the radius, radial and zSamples.
     */
    private void setGeometryData() {
        // allocate vertices
        vertCount = (zSamples - 2) * (radialSamples + 1) + 2;

        FloatBuffer posBuf = BufferUtils.createVector3Buffer(vertCount);

        // allocate normals if requested
        FloatBuffer normBuf = BufferUtils.createVector3Buffer(vertCount);

        // allocate texture coordinates
        FloatBuffer texBuf = BufferUtils.createVector2Buffer(vertCount);

        setBuffer(Type.Position, 3, posBuf);
        setBuffer(Type.Normal, 3, normBuf);
        setBuffer(Type.TexCoord, 2, texBuf);

        // generate geometry
        float fInvRS = 1.0f / radialSamples;
        float fZFactor = 2.0f / (zSamples - 1);

        // Generate points on the unit circle to be used in computing the mesh
        // points on a sphere slice.
        float[] afSin = new float[(radialSamples + 1)];
        float[] afCos = new float[(radialSamples + 1)];
        for (int iR = 0; iR < radialSamples; iR++) {
            float fAngle = FastMath.TWO_PI * fInvRS * iR;
            afCos[iR] = FastMath.cos(fAngle);
            afSin[iR] = FastMath.sin(fAngle);
        }
        afSin[radialSamples] = afSin[0];
        afCos[radialSamples] = afCos[0];

        TempVars vars = TempVars.get();
        Vector3f tempVa = vars.vect1;
        Vector3f tempVb = vars.vect2;
        Vector3f tempVc = vars.vect3;

        // generate the sphere itself
        int i = 0;
        for (int iZ = 1; iZ < (zSamples - 1); iZ++) {
            float fAFraction = FastMath.HALF_PI * (-1.0f + fZFactor * iZ); // in (-pi/2, pi/2)
            float fZFraction;
            if (useEvenSlices) {
                fZFraction = -1.0f + fZFactor * iZ; // in (-1, 1)
            } else {
                fZFraction = FastMath.sin(fAFraction); // in (-1,1)
            }
            float fZ = radius * fZFraction;

            // compute center of slice
            Vector3f kSliceCenter = tempVb.set(Vector3f.ZERO);
            kSliceCenter.z += fZ;

            // compute radius of slice
            float fSliceRadius = FastMath.sqrt(FastMath.abs(radius * radius
                    - fZ * fZ));

            // compute slice vertices with duplication at end point
            Vector3f kNormal;
            int iSave = i;
            for (int iR = 0; iR < radialSamples; iR++) {
                float fRadialFraction = iR * fInvRS; // in [0,1)
                Vector3f kRadial = tempVc.set(afCos[iR], afSin[iR], 0);
                kRadial.mult(fSliceRadius, tempVa);
                posBuf.put(kSliceCenter.x + tempVa.x).put(
                        kSliceCenter.y + tempVa.y).put(
                        kSliceCenter.z + tempVa.z);

                BufferUtils.populateFromBuffer(tempVa, posBuf, i);
                kNormal = tempVa;
                kNormal.normalizeLocal();
                if (!interior) // allow interior texture vs. exterior
                {
                    normBuf.put(kNormal.x).put(kNormal.y).put(
                            kNormal.z);
                } else {
                    normBuf.put(-kNormal.x).put(-kNormal.y).put(
                            -kNormal.z);
                }

                if (textureMode == TextureMode.Original) {
                    texBuf.put(fRadialFraction).put(
                            0.5f * (fZFraction + 1.0f));
                } else if (textureMode == TextureMode.Projected) {
                    texBuf.put(fRadialFraction).put(
                            FastMath.INV_PI
                            * (FastMath.HALF_PI + FastMath.asin(fZFraction)));
                } else if (textureMode == TextureMode.Polar) {
                    float r = (FastMath.HALF_PI - FastMath.abs(fAFraction)) / FastMath.PI;
                    float u = r * afCos[iR] + 0.5f;
                    float v = r * afSin[iR] + 0.5f;
                    texBuf.put(u).put(v);
                }

                i++;
            }

            BufferUtils.copyInternalVector3(posBuf, iSave, i);
            BufferUtils.copyInternalVector3(normBuf, iSave, i);

            if (textureMode == TextureMode.Original) {
                texBuf.put(1.0f).put(
                        0.5f * (fZFraction + 1.0f));
            } else if (textureMode == TextureMode.Projected) {
                texBuf.put(1.0f).put(
                        FastMath.INV_PI
                        * (FastMath.HALF_PI + FastMath.asin(fZFraction)));
            } else if (textureMode == TextureMode.Polar) {
                float r = (FastMath.HALF_PI - FastMath.abs(fAFraction)) / FastMath.PI;
                texBuf.put(r + 0.5f).put(0.5f);
            }

            i++;
        }

        vars.release();

        // south pole
        posBuf.position(i * 3);
        posBuf.put(0f).put(0f).put(-radius);

        normBuf.position(i * 3);
        if (!interior) {
            normBuf.put(0).put(0).put(-1); // allow for inner
        } // texture orientation
        // later.
        else {
            normBuf.put(0).put(0).put(1);
        }

        texBuf.position(i * 2);

        if (textureMode == TextureMode.Polar) {
            texBuf.put(0.5f).put(0.5f);
        } else {
            texBuf.put(0.5f).put(0.0f);
        }

        i++;

        // north pole
        posBuf.put(0).put(0).put(radius);

        if (!interior) {
            normBuf.put(0).put(0).put(1);
        } else {
            normBuf.put(0).put(0).put(-1);
        }

        if (textureMode == TextureMode.Polar) {
            texBuf.put(0.5f).put(0.5f);
        } else {
            texBuf.put(0.5f).put(1.0f);
        }

        updateBound();
    }

    /**
     * sets the indices for rendering the sphere.
     */
    private void setIndexData() {
        // allocate connectivity
        triCount = 2 * (zSamples - 2) * radialSamples;
        ShortBuffer idxBuf = BufferUtils.createShortBuffer(3 * triCount);
        setBuffer(Type.Index, 3, idxBuf);

        // generate connectivity
        int index = 0;
        for (int iZ = 0, iZStart = 0; iZ < (zSamples - 3); iZ++) {
            int i0 = iZStart;
            int i1 = i0 + 1;
            iZStart += (radialSamples + 1);
            int i2 = iZStart;
            int i3 = i2 + 1;
            for (int i = 0; i < radialSamples; i++, index += 6) {
                if (!interior) {
                    idxBuf.put((short) i0++);
                    idxBuf.put((short) i1);
                    idxBuf.put((short) i2);
                    idxBuf.put((short) i1++);
                    idxBuf.put((short) i3++);
                    idxBuf.put((short) i2++);
                } else { // inside view
                    idxBuf.put((short) i0++);
                    idxBuf.put((short) i2);
                    idxBuf.put((short) i1);
                    idxBuf.put((short) i1++);
                    idxBuf.put((short) i2++);
                    idxBuf.put((short) i3++);
                }
            }
        }

        // south pole triangles
        for (int i = 0; i < radialSamples; i++, index += 3) {
            if (!interior) {
                idxBuf.put((short) i);
                idxBuf.put((short) (vertCount - 2));
                idxBuf.put((short) (i + 1));
            } else { // inside view
                idxBuf.put((short) i);
                idxBuf.put((short) (i + 1));
                idxBuf.put((short) (vertCount - 2));
            }
        }

        // north pole triangles
        int iOffset = (zSamples - 3) * (radialSamples + 1);
        for (int i = 0; i < radialSamples; i++, index += 3) {
            if (!interior) {
                idxBuf.put((short) (i + iOffset));
                idxBuf.put((short) (i + 1 + iOffset));
                idxBuf.put((short) (vertCount - 1));
            } else { // inside view
                idxBuf.put((short) (i + iOffset));
                idxBuf.put((short) (vertCount - 1));
                idxBuf.put((short) (i + 1 + iOffset));
            }
        }
    }

    /**
     * @param textureMode
     *            The textureMode to set.
     */
    public void setTextureMode(TextureMode textureMode) {
        this.textureMode = textureMode;
        setGeometryData();
    }

    /**
     * Changes the information of the sphere into the given values.
     * 
     * @param zSamples the number of zSamples of the sphere.
     * @param radialSamples the number of radial samples of the sphere.
     * @param radius the radius of the sphere.
     */
    public void updateGeometry(int zSamples, int radialSamples, float radius) {
        if (zSamples < 3) {
            throw new IllegalArgumentException("zSamples cannot be smaller than 3");
        }
        updateGeometry(zSamples, radialSamples, radius, false, false);
    }

    public void updateGeometry(int zSamples, int radialSamples, float radius, boolean useEvenSlices, boolean interior) {
        if (zSamples < 3) {
            throw new IllegalArgumentException("zSamples cannot be smaller than 3");
        }
        this.zSamples = zSamples;
        this.radialSamples = radialSamples;
        this.radius = radius;
        this.useEvenSlices = useEvenSlices;
        this.interior = interior;
        setGeometryData();
        setIndexData();
        setStatic();
    }

    public void read(JmeImporter e) throws IOException {
        super.read(e);
        InputCapsule capsule = e.getCapsule(this);
        zSamples = capsule.readInt("zSamples", 0);
        radialSamples = capsule.readInt("radialSamples", 0);
        radius = capsule.readFloat("radius", 0);
        useEvenSlices = capsule.readBoolean("useEvenSlices", false);
        textureMode = capsule.readEnum("textureMode", TextureMode.class, TextureMode.Original);
        interior = capsule.readBoolean("interior", false);
    }

    public void write(JmeExporter e) throws IOException {
        super.write(e);
        OutputCapsule capsule = e.getCapsule(this);
        capsule.write(zSamples, "zSamples", 0);
        capsule.write(radialSamples, "radialSamples", 0);
        capsule.write(radius, "radius", 0);
        capsule.write(useEvenSlices, "useEvenSlices", false);
        capsule.write(textureMode, "textureMode", TextureMode.Original);
        capsule.write(interior, "interior", false);
    }
}
