/**
 * Make a donation http://sourceforge.net/donate/index.php?group_id=98797
 * Microcrowd.com
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 * 
 * This library 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
 * Lesser General Public License for more details.
 * 
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 * 
 * Contact Josh DeFord jdeford@microcrowd.com
 */

package com.microcrowd.loader.java3d.max3ds.chunks;

import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Set;
import javax.media.j3d.Appearance;
import javax.media.j3d.Geometry;
import javax.media.j3d.Material;
import javax.media.j3d.Shape3D;
import javax.media.j3d.TransformGroup;
import javax.vecmath.Color3f;
import javax.vecmath.Point3f;
import javax.vecmath.TexCoord2f;
import javax.vecmath.Vector3f;
import com.microcrowd.loader.java3d.max3ds.ChunkChopper;
import com.microcrowd.loader.java3d.max3ds.ChunkMap;
import com.sun.j3d.utils.geometry.GeometryInfo;
import com.sun.j3d.utils.geometry.NormalGenerator;
import com.sun.j3d.utils.geometry.Stripifier;

/**
 * This chunk describes all the triangles that make up a mesh.
 * Each triangle is defined in terms of three indexes each of which
 * is a point reference to a vertex in the vertex list loaded
 * by the triangular mesh chunk.
 * After loading the Smoothing chunk the normals for the mesh
 * are generated accordingly.
 */
public class FacesDescriptionChunk extends Chunk
{
    public static final Appearance DEFAULT_APPEARANCE;

    private Point3f[] currentVertices;
    private TexCoord2f[] textureTriangles;
    private PointMapper shareMap;

    static {
        DEFAULT_APPEARANCE= new Appearance();
        Material defaultMaterial = new Material();
        defaultMaterial.setAmbientColor(new Color3f(.5f, .5f, .5f));
        //defaultMaterial.setDiffuseColor(new Color3f(.5f, .5f, .5f));
        //defaultMaterial.setSpecularColor(new Color3f(.5f, .5f, .5f));
        DEFAULT_APPEARANCE.setMaterial(defaultMaterial);
    }



    /**
     * Maintains a two way mapping between coordinates
     * and vertices.  A coordinate to vertex is one to many 
     * Vertex to coordinate is one to one.
     * In this class we maintain the definition that a coordinate
     * is a point in 3D space and a vertex is a coordinate serving
     * as one of three defining a face.
     */
    private class PointMapper extends HashMap
    {
        private Set[] coordinateSet;
        /**
         * Constructs a PointMapper with a
         * the number of coordinates initialized to size.
         * @param size the number of coordinates in the set.
         */
        public PointMapper(int size)
        {
            coordinateSet = new Set[size];
        }

        /**
         * Adds an index for a coordinate to the set of vertices mapped
         * to that coordinate. All coordinates may have one or more vertices
         * that use them.  
         * @param coordinate the coordinate being mapped to the vertexNum 
         * @param vertexNum the number of the vertex using the coordinate
         */
        public void addCoordinate(Point3f coordinate, int vertexNum)
        {
            Set sharedCoordinates = (Set)get(coordinate); 
            if(sharedCoordinates == null)
            {
                sharedCoordinates = new HashSet();
                put(coordinate, sharedCoordinates);
            }
            sharedCoordinates.add(new Integer(vertexNum));
            coordinateSet[vertexNum] = sharedCoordinates;
        }

        /**
         * Gets all the coordinates for a particular vertex that
         * also share that vertex after the smoothing groups have been
         * accounted for.  Any coordinates that are not both shared
         * by the vertex and do not share a smoothing group with the coordinate
         * will not be returned.
         * @param coordinateNum the number of the coordinate to get the set
         * of vertices for that share it.
         * @param smoothGroups the group of coordinates used to filter out the 
         * non-shared vertices.
         */
        public Set getSharedCoordinates(int coordinateNum, int[] smoothGroups)
        {
            Set returnSet = new HashSet();
            Set sharingVertices = coordinateSet[coordinateNum];
            Iterator vertices = sharingVertices.iterator();
            int coordinateMask = smoothGroups[coordinateNum];
            while(vertices.hasNext())
            {
                Integer vertex = (Integer)vertices.next();
                int nextMask = smoothGroups[vertex.intValue()];
                if((nextMask & coordinateMask) != 0)
                {
                    returnSet.add(vertex);
                }
            }
            return returnSet; 
        }
    }

    /**
     * Reads the number of faces from the ChunkChopper.
     * For each face read three shorts representing
     * indices of vertices loaded by the TriangularMeshChunk
     *
     * @param chopper chopper the has the data  
     */
    public void loadData(ChunkChopper chopper)
    {
        int numFaces = chopper.getUnsignedShort();
        shareMap = new PointMapper(numFaces*3);
        Point3f[] coordinates = (Point3f[])chopper.popData(ChunkMap.VERTEX_LIST);
        TexCoord2f[] texturePoints = (TexCoord2f[])chopper.popData(ChunkMap.TEXTURE_COORDINATES);

        currentVertices = new Point3f[numFaces * 3];
        chopper.pushData(chopper.getID(), currentVertices);
        if (texturePoints != null) 
        {
            textureTriangles = new TexCoord2f[numFaces * 3];
        }

        for (int i = 0; i < numFaces; i++) {
            int vertexIndex = i * 3;
            int index0 = chopper.getUnsignedShort();
            int index1 = chopper.getUnsignedShort();
            int index2 = chopper.getUnsignedShort();

            currentVertices[vertexIndex] = coordinates[index0];
            currentVertices[vertexIndex + 1] = coordinates[index1];
            currentVertices[vertexIndex + 2] = coordinates[index2];

            shareMap.addCoordinate(coordinates[index0], vertexIndex);
            shareMap.addCoordinate(coordinates[index1], vertexIndex+1);
            shareMap.addCoordinate(coordinates[index2], vertexIndex+2);


            if (textureTriangles != null) {
                textureTriangles[vertexIndex] = texturePoints[index0];
                textureTriangles[vertexIndex + 1] = texturePoints[index1];
                textureTriangles[vertexIndex + 2] = texturePoints[index2];
            }

            //This is a bit masked value that is used to determine which edges are visible... not needed.
            chopper.getUnsignedShort(); 
        }
    }

    /**
     * Loads a mesh onto the scene graph with the specified data
     * from subchunks.
     * If there is no material, this will put a default
     * material on the shape.
     */
    public void initialize(ChunkChopper chopper)
    {
        final String materialName = (String)chopper.popData(ChunkMap.FACES_MATERIAL);
        final int[]  smoothGroups = (int[])chopper.popData(ChunkMap.SMOOTH);
        Shape3D      shape        = new Shape3D();
        GeometryInfo geometryInfo = new GeometryInfo(GeometryInfo.TRIANGLE_ARRAY);

        geometryInfo.setCoordinates(currentVertices);
        TransformGroup transformGroup = (TransformGroup)chopper.getGroup();
        transformGroup.addChild(shape);

        if (textureTriangles != null) 
        {
            geometryInfo.setTextureCoordinateParams(1, 2);
            geometryInfo.setTextureCoordinates(0, textureTriangles);
        }

        if(materialName != null)
        {
            shape.setAppearance((Appearance)chopper.getNamedObject(materialName));
        }
        else
        {
            shape.setAppearance(DEFAULT_APPEARANCE);
        }
        if(smoothGroups == null)
        {
            NormalGenerator normalGenerator = new NormalGenerator();
            geometryInfo.recomputeIndices();
            normalGenerator.generateNormals(geometryInfo);
        }
        else
        {
            Vector3f[] normals = generateNormals(currentVertices);
            Vector3f[] smoothNormals = smoothNormals(normals, shareMap, smoothGroups);
            geometryInfo.setNormals(smoothNormals);
        }

        new Stripifier().stripify(geometryInfo);
        shape.setGeometry(geometryInfo.getGeometryArray());
        shape.setCapability(Geometry.ALLOW_INTERSECT);
        com.sun.j3d.utils.picking.PickTool.setCapabilities(shape, com.sun.j3d.utils.picking.PickTool.INTERSECT_FULL);

        currentVertices=null;
        textureTriangles=null;
    }

    /**
     * Takes all the normals for all the vertices and averages them with
     * normals with which they share a coordinate and at least one smooth group.
     * @param currentNormals the normals for each face.
     * @param sharedPoints the point mapper that will choose which points are 
     * and which are not shared.
     * @param smoothGroups the indexed list of group masks loaded by the smooth chunk.
     * @return normals averaged among the shared vertices in their smoothing groups.
     */
    public Vector3f[] smoothNormals(Vector3f[] currentNormals, PointMapper sharedPoints, int[] smoothGroups)
    {
        Vector3f[] smoothNormals = new Vector3f[currentNormals.length];
        for(int i=0; i < currentNormals.length; i++)
        {
            Set otherPoints = sharedPoints.getSharedCoordinates(i, smoothGroups);
            if(otherPoints != null)
            {
                Vector3f[] sharedNormals = new Vector3f[otherPoints.size()]; 
                Iterator pointIterator = otherPoints.iterator();
                for(int j = 0; j < sharedNormals.length; j++)
                {
                    sharedNormals[j] = currentNormals[((Integer)pointIterator.next()).intValue()];
                }
                smoothNormals[i] = averageNormals(sharedNormals);
            }
            else
            {
                smoothNormals[i] = currentNormals[i];
            }
        }
        return smoothNormals;
    }

    /**
     * Averages the normals provided in order to provide
     * smooth, noncreased appearances for meshes.
     * @param normals the normals that should be averaged
     * @return a normalized normal that can be used in place
     * of all the normals provided.
     */
    public Vector3f averageNormals(Vector3f[] normals)
    {
        Vector3f newNormal = new Vector3f();
        for(int i=0; i < normals.length; i++)
        {
            newNormal.add(normals[i]);
        }
        newNormal.normalize();
        return newNormal;
    }

    /**
     * Generates normals for each vertex of each
     * face that are absolutely normal to the face.
     * @param point0 The first point of the face
     * @param point1 The second point of the face
     * @param point2 The third point of the face
     * @return the three normals that should be 
     * used for the triangle represented by the parameters.
     */
    private Vector3f[] generateNormals(Point3f points[])
    {
        Vector3f[] normals = new Vector3f[points.length];
        for(int i=0; i < normals.length;)
        {
            Vector3f normal    = new Vector3f();
            Vector3f v1        = new Vector3f();
            Vector3f v2        = new Vector3f();
    
            v1.sub(points[i+1], points[i]);
            v2.sub(points[i+2], points[i]);
            normal.cross(v1, v2);
            normal.normalize();

    
            normals[i++] = new Vector3f(normal);
            normals[i++] = new Vector3f(normal);
            normals[i++] = new Vector3f(normal);
        }

        return normals;
    }
}
