/*
 * Copyright (C) 2013-2015 F(X)yz, 
 * Sean Phillips, Jason Pollastrini and Jose Pereda
 * All rights reserved.
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
package org.fxyz.shapes.primitives;

import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.Collectors;
import java.util.stream.DoubleStream;
import java.util.stream.IntStream;
import javafx.beans.property.FloatProperty;
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.SimpleFloatProperty;
import javafx.beans.property.SimpleIntegerProperty;
import javafx.geometry.Point2D;
import javafx.scene.DepthTest;
import javafx.scene.shape.CullFace;
import javafx.scene.shape.DrawMode;
import javafx.scene.shape.TriangleMesh;
import org.fxyz.geometry.Face3;
import org.fxyz.geometry.Point3D;
import org.fxyz.utils.FloatCollector;
import org.fxyz.shapes.primitives.helper.TriangleMeshHelper.TextureType;

/**
 *
 * @author jpereda
 */
public class IcosahedronMesh extends TexturedMesh {
    private final static int DEFAULT_LEVEL = 1;
    private final static float SPHERE_DIAMETER =  1f;
    
    public IcosahedronMesh(){
        this(DEFAULT_LEVEL,SPHERE_DIAMETER);
    }
    public IcosahedronMesh(int level){
        this(level,SPHERE_DIAMETER);
    }
    public IcosahedronMesh(float diameter){
        this(DEFAULT_LEVEL,diameter);
    }
    public IcosahedronMesh(int level, float diameter){
        setLevel(level);
        setDiameter(diameter);

        updateMesh();
        setCullFace(CullFace.BACK);
        setDrawMode(DrawMode.FILL);
        setDepthTest(DepthTest.ENABLE);
        
        diameterProperty().addListener((obs,f0,f1)->{
            if(mesh!=null && f0!=null && f1!=null && f0.floatValue()>0 && f1.floatValue()>0){
                updateVertices(f1.floatValue()/f0.floatValue());
            }
        });
        
        levelProperty().addListener((obs,i0,i1)->{
            if(mesh!=null && i1!=null && i1.intValue()>=0){
                updateMesh();
            }
        });
    }
    
    @Override
    protected final void updateMesh(){       
        setMesh(null);
        mesh=createSphere(level.get(), diameter.get());
        setMesh(mesh);
    }
    private final FloatProperty diameter = new SimpleFloatProperty(SPHERE_DIAMETER);

    public final float getDiameter() {
        return diameter.get();
    }

    public final void setDiameter(float value) {
        diameter.set(value);
    }

    public final FloatProperty diameterProperty() {
        return diameter;
    }
    
    private final IntegerProperty level = new SimpleIntegerProperty(DEFAULT_LEVEL);

    public final int getLevel() {
        return level.get();
    }

    public final void setLevel(int value) {
        level.set(value);
    }

    public final IntegerProperty levelProperty() {
        return level;
    }
    
    /*
        ICOSAHEDRON 
    */
    private final float[] baseVertices = new float[]{
        -0.525731f,  0.850651f, 0.f,
         0.525731f,  0.850651f, 0.f, 
        -0.525731f, -0.850651f, 0.f,
         0.525731f, -0.850651f, 0.f, 
        0.f, -0.525731f,  0.850651f, 
        0.f,  0.525731f,  0.850651f, 
        0.f, -0.525731f, -0.850651f, 
        0.f,  0.525731f, -0.850651f, 
         0.850651f, 0.f, -0.525731f, 
         0.850651f, 0.f,  0.525731f, 
        -0.850651f, 0.f, -0.525731f, 
        -0.850651f, 0.f,  0.525731f
    };
    
    private final float[] baseTexCoords = new float[]{
            0.181818f, 0f,             0.363636f, 0f, 
            0.545455f, 0f,             0.727273f, 0f, 
            0.909091f, 0f,             0.0909091f, 0.333333f,
            0.272727f, 0.333333f,      0.454545f, 0.333333f, 
            0.636364f, 0.333333f,      0.818182f, 0.333333f, 
            1f, 0.333333f,             0f, 0.666667f, 
            0.181818f, 0.666667f,      0.363636f, 0.666667f, 
            0.545455f, 0.666667f,      0.727273f, 0.666667f, 
            0.909091f, 0.666667f,      0.0909091f, 1f, 
            0.272727f, 1f,             0.454545f, 1f, 
            0.636364f, 1f,             0.818182f, 1f
    };
    
    private final int[] baseTexture = new int[]{
            5,11,12,            5,12,6,             5,6,0,             10,4,9,
            10,9,16,            6,12,13,            12,11,17,          16,9,15,
            9,3,8,              1,6,7,              14,13,19,          14,20,15,
            14,15,8,            14,8,7,             14,7,13,           18,13,12,
            15,21,16,           8,15,9,             7,8,2,             13,7,6
    };
    
    private final List<Integer> baseFaces = Arrays.asList(
            0,11,5,             0,5,1,             0,1,7,             0,7,10,
            0,10,11,            1,5,9,             5,11,4,            11,10,2,
            10,7,6,             7,1,8,             3,9,4,             3,4,2,
            3,2,6,              3,6,8,             3,8,9,             4,9,5,
            2,4,11,             6,2,10,            8,6,7,             9,8,1
    );
    /*
        ICOSPHERE
    */
    private int numVertices, numTexCoords, numFaces;
    private float[] points0, texCoord0;
    private int[] faces0;
    private List<Point2D> texCoord1;
    
    private TriangleMesh createSphere(int level, float diameter) {
        TriangleMesh m0=null;
        if(level>0){
            m0= createSphere(level-1, diameter);
        }
        
        // read vertices from level-1
        if(level==0){
            points0 = baseVertices; 
            numVertices=baseVertices.length/3;
        } else if(m0!=null) {
            points0=new float[numVertices*m0.getPointElementSize()];
            m0.getPoints().toArray(points0);
        }

        List<Point3D> points1 = IntStream.range(0, numVertices)
                        .mapToObj(i -> new Point3D(points0[3*i], points0[3*i+1], points0[3*i+2]))
                        .collect(Collectors.toList());

        // read textures from level -1
        if(level==0){
            texCoord0 = baseTexCoords;
            numTexCoords=baseTexCoords.length/2;
        } else if(m0!=null){
            texCoord0=new float[numTexCoords*m0.getTexCoordElementSize()];
            m0.getTexCoords().toArray(texCoord0);
        }

        texCoord1 = IntStream.range(0, numTexCoords)
                    .mapToObj(i -> new Point2D(texCoord0[2*i], texCoord0[2*i+1]))
                    .collect(Collectors.toList());
        
        // read faces from level -1
        if(level==0){
            faces0 = IntStream.range(0, baseFaces.size()/3)
                        .mapToObj(i->IntStream.of(baseFaces.get(3*i), baseTexture[3*i], 
                                baseFaces.get(3*i+1), baseTexture[3*i+1], 
                                baseFaces.get(3*i+2), baseTexture[3*i+2]))
                        .flatMapToInt(i->i).toArray();
            numFaces=baseFaces.size()/3;
        } else if(m0!=null){
            faces0=new int[numFaces*m0.getFaceElementSize()];
            m0.getFaces().toArray(faces0);
        }
        
        List<Face3> faces1 = IntStream.range(0, numFaces)
                    .mapToObj(i -> new Face3(faces0[6*i], faces0[6*i+2], faces0[6*i+4]))
                    .collect(Collectors.toList());

        index.set(points1.size());
        map.clear();
        listVertices.clear();
        listFaces.clear();
        listVertices.addAll(points1);
        
        faces1.forEach(face->{
            int v1=face.p0;
            int v2=face.p1;
            int v3=face.p2;
            if(level>0){
                int a = getMiddle(v1,points1.get(v1),v2,points1.get(v2));
                int b = getMiddle(v2,points1.get(v2),v3,points1.get(v3));
                int c = getMiddle(v3,points1.get(v3),v1,points1.get(v1));

                listFaces.add(new Face3(v1,a,c));
                listFaces.add(new Face3(v2,b,a));
                listFaces.add(new Face3(v3,c,b));
                listFaces.add(new Face3(a,b,c));
            } else {
                listFaces.add(new Face3(v1,v2,v3));
            }
        });
        map.clear();
        numVertices=listVertices.size();
        numFaces=listFaces.size();
        
        List<Face3> textures1;
        if(level==0){
            textures1= IntStream.range(0, faces0.length/6)
                    .mapToObj(i -> new Face3(faces0[6*i+1], faces0[6*i+3], faces0[6*i+5]))
                    .collect(Collectors.toList());
        } else {
            textures1 = listTextures.stream().map(t->t).collect(Collectors.toList());
        }
        
        index.set(texCoord1.size());
        listTextures.clear();
        textures1.forEach(face->{
            int v1=face.p0;
            int v2=face.p1;
            int v3=face.p2;
            if(level>0){
                int a = getMiddle(v1,texCoord1.get(v1),v2,texCoord1.get(v2));
                int b = getMiddle(v2,texCoord1.get(v2),v3,texCoord1.get(v3));
                int c = getMiddle(v3,texCoord1.get(v3),v1,texCoord1.get(v1));

                listTextures.add(new Face3(v1,a,c));
                listTextures.add(new Face3(v2,b,a));
                listTextures.add(new Face3(v3,c,b));
                listTextures.add(new Face3(a,b,c));
            } else {
                listTextures.add(new Face3(v1,v2,v3));
            }
        });
        map.clear();

        texCoord0=texCoord1.stream().flatMapToDouble(p->DoubleStream.of(p.getX(),p.getY()))
                .collect(()->new FloatCollector(texCoord1.size()*2), FloatCollector::add, FloatCollector::join).toArray();
        numTexCoords=texCoord0.length/2;
        textureCoords=texCoord0;
        if(level==getLevel()){
            areaMesh.setWidth(Math.PI*diameter);
            areaMesh.setHeight(Math.PI*diameter);
            rectMesh.setWidth((int)Math.sqrt(texCoord0.length));
            rectMesh.setHeight(texCoord0.length/((int)Math.sqrt(texCoord0.length)));
        }
        return createMesh();
    }
    
    private final AtomicInteger index = new AtomicInteger();
    private final HashMap<String, Integer> map = new HashMap<>();

    private int getMiddle(int v1, Point3D p1, int v2, Point3D p2){
        String key = ""+Math.min(v1,v2)+"_"+Math.max(v1,v2);
        if(map.get(key)!=null){
            return map.get(key);
        }

        listVertices.add(p1.add(p2).multiply(0.5f).normalize());

        map.put(key,index.get());
        return index.getAndIncrement();
    }
    
    private int getMiddle(int v1, Point2D p1, int v2, Point2D p2){
        String key = ""+Math.min(v1,v2)+"_"+Math.max(v1,v2);
        if(map.get(key)!=null){
            return map.get(key);
        }

        texCoord1.add(p1.add(p2).multiply(0.5f));

        map.put(key,index.get());
        return index.getAndIncrement();
    }

}