/**
 * SpheroidMesh.java
 *
 * Copyright (c) 2013-2016, F(X)yz
 * 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 F(X)yz, any associated website, 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 F(X)yz 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 org.fxyz3d.shapes.primitives;

import java.util.Objects;
import javafx.beans.property.DoubleProperty;
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.SimpleDoubleProperty;
import javafx.beans.property.SimpleIntegerProperty;
import javafx.scene.DepthTest;
import javafx.scene.shape.CullFace;
import javafx.scene.shape.TriangleMesh;

/**
 *
 * @author Dub
 */
public class SpheroidMesh extends TexturedMesh{
    
    private static final double DEFAULT_MAJOR_RADIUS = 50f;
    private static final double DEFAULT_MINOR_RADIUS = 12f;
    private static final int   DEFAULT_DIVISIONS = 64;

    public SpheroidMesh() {
        setMesh(createSpheroid(getDivisions(), getMajorRadius(), getMinorRadius()));
        setCullFace(CullFace.BACK);
        setDepthTest(DepthTest.ENABLE);
    }   

    /**
     * 
     * @param radius Creates a Sphere with the specified Radius
     */
    public SpheroidMesh(double radius) {
        this();
        setMajorRadius(radius);
        setMinorRadius(radius);
    }
    /**
     * 
     * @param majRad The major(horizontal) radius
     * @param minRad The minor(vertical) radius
     */
    public SpheroidMesh(double majRad, double minRad) {
        this();
        setMajorRadius(majRad);
        setMinorRadius(minRad);
    }
    
    /**
     * 
     * @param divs  Divisions for the Spheroid. Default is 64
     * @param majRad The major(horizontal) radius
     * @param minRad The minor(vertical) radius
     */
    public SpheroidMesh(int divs, double majRad, double minRad) {
        this();
        setDivisions(divs);
        setMajorRadius(majRad);
        setMinorRadius(minRad);
    }
    
    public boolean isSphere(){
        return Objects.equals(getMajorRadius(), getMinorRadius());
    }
    public boolean isOblateSpheroid(){
        return getMajorRadius() > getMinorRadius();
    }
    public boolean isProlateSpheroid(){
        return getMajorRadius() < getMinorRadius();
    }
    
    private TriangleMesh createSpheroid(int divs, double major, double minor) {
        divs = correctDivisions(divs);
        TriangleMesh m = new TriangleMesh();
        
        final int divsHalf = divs / 2;

        final int numPoints = divs * (divsHalf - 1) + 2;
        final int numTexCoords = (divs + 1) * (divsHalf - 1) + divs * 2;
        final int numFaces = divs * (divsHalf - 2) * 2 + divs * 2;

        final float divf = 1.f / divs;

        float points[] = new float[numPoints * m.getPointElementSize()];
        float tPoints[] = new float[numTexCoords * m.getTexCoordElementSize()];
        int faces[] = new int[numFaces * m.getFaceElementSize()];

        int pPos = 0, tPos = 0;

        for (int lat = 0; lat < divsHalf-1 ; ++lat) {
            
            float latRad = divf * (lat + 1 - divsHalf / 2) * 2 * (float) Math.PI;
            float sin_v = (float) Math.sin(latRad);
            float cos_v = (float) Math.cos(latRad);

            float ty = 0.5f + sin_v * 0.5f;
            
            for (int lon = 0; lon < divs; ++lon) {
                
                double lonRad = divf * lon * 2 * (float) Math.PI;
                float sin_u = (float) Math.sin(lonRad);
                float cos_u = (float) Math.cos(lonRad);
                
                points[pPos + 0] = (float) (cos_v * cos_u * major); // x
                points[pPos + 2] = (float) (cos_v * sin_u * major); // z
                points[pPos + 1] = (float) (sin_v * minor);        // y up 
                
                tPoints[tPos + 0] = 1 - divf * lon;
                tPoints[tPos + 1] = ty;
                pPos += 3;
                tPos += 2;
            }
            tPoints[tPos + 0] = 0;
            tPoints[tPos + 1] = ty;
            tPos += 2;
        }

        points[pPos + 0] = 0;
        points[pPos + 1] = (float) -minor;
        points[pPos + 2] = 0;
        points[pPos + 3] = 0;
        points[pPos + 4] = (float) minor;
        points[pPos + 5] = 0;
        pPos += 6;

        int pS = (divsHalf - 1) * divs;

        float textureDelta = 1.f / 256;
        for (int i = 0; i < divs; ++i) {
            tPoints[tPos + 0] = divf * (0.5f + i);
            tPoints[tPos + 1] = textureDelta;
            tPos += 2;
        }

        for (int i = 0; i < divs; ++i) {
            tPoints[tPos + 0] = divf * (0.5f + i);
            tPoints[tPos + 1] = 1 - textureDelta;
            tPos += 2;
        }

        int fIndex = 0;
        for (int y = 0; y < divsHalf - 2; ++y) {
            for (int x = 0; x < divs; ++x) {
                int p0 = y * divs + x;
                int p1 = p0 + 1;
                int p2 = p0 + divs;
                int p3 = p1 + divs;

                int t0 = p0 + y;
                int t1 = t0 + 1;
                int t2 = t0 + (divs + 1);
                int t3 = t1 + (divs + 1);

                // add p0, p1, p2
                faces[fIndex + 0] = p0;
                faces[fIndex + 1] = t0;
                faces[fIndex + 2] = p1 % divs == 0 ? p1 - divs : p1;
                faces[fIndex + 3] = t1;
                faces[fIndex + 4] = p2;
                faces[fIndex + 5] = t2;
                fIndex += 6;

                // add p3, p2, p1
                faces[fIndex + 0] = p3 % divs == 0 ? p3 - divs : p3;
                faces[fIndex + 1] = t3;
                faces[fIndex + 2] = p2;
                faces[fIndex + 3] = t2;
                faces[fIndex + 4] = p1 % divs == 0 ? p1 - divs : p1;
                faces[fIndex + 5] = t1;
                fIndex += 6;
            }
        }

        int p0 = pS;
        int tB = (divsHalf - 1) * (divs + 1);
        for (int x = 0; x < divs; ++x) {
            int p2 = x, p1 = x + 1, t0 = tB + x;
            faces[fIndex + 0] = p0;
            faces[fIndex + 1] = t0;
            faces[fIndex + 2] = p1 == divs ? 0 : p1;
            faces[fIndex + 3] = p1;
            faces[fIndex + 4] = p2;
            faces[fIndex + 5] = p2;
            fIndex += 6;
        }

        p0 = p0 + 1;
        tB = tB + divs;
        int pB = (divsHalf - 2) * divs;

        for (int x = 0; x < divs; ++x) {
            int p1 = pB + x, p2 = pB + x + 1, t0 = tB + x;
            int t1 = (divsHalf - 2) * (divs + 1) + x, t2 = t1 + 1;
            faces[fIndex + 0] = p0;
            faces[fIndex + 1] = t0;
            faces[fIndex + 2] = p1;
            faces[fIndex + 3] = t1;
            faces[fIndex + 4] = p2 % divs == 0 ? p2 - divs : p2;
            faces[fIndex + 5] = t2;
            fIndex += 6;
        }
        
        m.getPoints().addAll(points);
        m.getTexCoords().addAll(tPoints);
        m.getFaces().addAll(faces);
        
        return m;
    }
    
    private int correctDivisions(int div) {
        return ((div + 3) / 4) * 4;
    }
    /*
    
    */
    private final DoubleProperty majorRadius = new SimpleDoubleProperty(this, "majorRadius", DEFAULT_MAJOR_RADIUS){
        @Override
        protected void invalidated() {
            setMesh(createSpheroid(getDivisions(), getMajorRadius(), getMinorRadius()));
        }
    };
    public final Double getMajorRadius() {
        return majorRadius.get();
    }
    public final void setMajorRadius(Double value) {
        majorRadius.set(value);
    }
    public DoubleProperty majorRadiusProperty() {
        return majorRadius;
    }
    
    private final DoubleProperty minorRadius = new SimpleDoubleProperty(this, "minorRadius", DEFAULT_MINOR_RADIUS){
        @Override
        protected void invalidated() {
            setMesh(createSpheroid(getDivisions(), getMajorRadius(), getMinorRadius()));
        }
    };
    public final Double getMinorRadius() {
        return minorRadius.get();
    }
    public final void setMinorRadius(double value) {
        minorRadius.set(value);
    }
    public DoubleProperty minorRadiusProperty() {
        return minorRadius;
    }
    
    private final IntegerProperty divisions = new SimpleIntegerProperty(this, "divisions", DEFAULT_DIVISIONS){
        @Override
        protected void invalidated() {
            setMesh(createSpheroid(getDivisions(), getMajorRadius(), getMinorRadius()));
        }
    };
    public final int getDivisions() {
        return divisions.get();
    }
    public final void setDivisions(int value) {
        divisions.set(value);
    }
    public IntegerProperty divisionsProperty() {
        return divisions;
    }

    @Override
    protected void updateMesh() {
        setMesh(createSpheroid(getDivisions(), getMajorRadius(), getMinorRadius()));
    }
}