package com.jme3.scene.plugins.blender.meshes;

import java.nio.IntBuffer;
import java.nio.ShortBuffer;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;

import com.jme3.bounding.BoundingBox;
import com.jme3.bounding.BoundingVolume;
import com.jme3.material.Material;
import com.jme3.material.RenderState.FaceCullMode;
import com.jme3.math.FastMath;
import com.jme3.math.Vector2f;
import com.jme3.math.Vector3f;
import com.jme3.scene.Geometry;
import com.jme3.scene.Mesh;
import com.jme3.scene.Mesh.Mode;
import com.jme3.scene.Node;
import com.jme3.scene.VertexBuffer;
import com.jme3.scene.VertexBuffer.Type;
import com.jme3.scene.VertexBuffer.Usage;
import com.jme3.scene.plugins.blender.BlenderContext;
import com.jme3.scene.plugins.blender.BlenderContext.LoadedDataType;
import com.jme3.scene.plugins.blender.file.BlenderFileException;
import com.jme3.scene.plugins.blender.file.Structure;
import com.jme3.scene.plugins.blender.materials.MaterialContext;
import com.jme3.scene.plugins.blender.meshes.MeshBuffers.BoneBuffersData;
import com.jme3.scene.plugins.blender.modifiers.Modifier;
import com.jme3.scene.plugins.blender.objects.Properties;

/**
 * The class extends Geometry so that it can be temporalily added to the object's node.
 * Later each such node's child will be transformed into a list of geometries.
 * 
 * @author Marcin Roguski (Kaelthas)
 */
public class TemporalMesh extends Geometry {
    private static final Logger        LOGGER                    = Logger.getLogger(TemporalMesh.class.getName());
    /** A minimum weight value. */
    private static final double 	   MINIMUM_BONE_WEIGHT 		 = FastMath.DBL_EPSILON;
    
    /** The blender context. */
    protected final BlenderContext     blenderContext;

    /** The mesh's structure. */
    protected final Structure          meshStructure;

    /** Loaded vertices. */
    protected List<Vector3f>           vertices                  = new ArrayList<Vector3f>();
    /** Loaded normals. */
    protected List<Vector3f>           normals                   = new ArrayList<Vector3f>();
    /** Loaded vertex groups. */
    protected List<Map<String, Float>> vertexGroups              = new ArrayList<Map<String, Float>>();
    /** Loaded vertex colors. */
    protected List<byte[]>             verticesColors            = new ArrayList<byte[]>();

    /** Materials used by the mesh. */
    protected MaterialContext[]        materials;
    /** The properties of the mesh. */
    protected Properties               properties;
    /** The bone indexes. */
    protected Map<String, Integer>     boneIndexes               = new HashMap<String, Integer>();
    /** The modifiers that should be applied after the mesh has been created. */
    protected List<Modifier>           postMeshCreationModifiers = new ArrayList<Modifier>();

    /** The faces of the mesh. */
    protected List<Face>               faces                     = new ArrayList<Face>();
    /** The edges of the mesh. */
    protected List<Edge>               edges                     = new ArrayList<Edge>();
    /** The points of the mesh. */
    protected List<Point>              points                    = new ArrayList<Point>();
    /** A map between index and faces that contain it (for faster index - face queries). */
    protected Map<Integer, Set<Face>>  indexToFaceMapping        = new HashMap<Integer, Set<Face>>();
    /** A map between index and edges that contain it (for faster index - edge queries). */
    protected Map<Integer, Set<Edge>>  indexToEdgeMapping        = new HashMap<Integer, Set<Edge>>();

    /** The bounding box of the temporal mesh. */
    protected BoundingBox              boundingBox;

    /**
     * Creates a temporal mesh based on the given mesh structure.
     * @param meshStructure
     *            the mesh structure
     * @param blenderContext
     *            the blender context
     * @throws BlenderFileException
     *             an exception is thrown when problems with file reading occur
     */
    public TemporalMesh(Structure meshStructure, BlenderContext blenderContext) throws BlenderFileException {
        this(meshStructure, blenderContext, true);
    }

    /**
     * Creates a temporal mesh based on the given mesh structure.
     * @param meshStructure
     *            the mesh structure
     * @param blenderContext
     *            the blender context
     * @param loadData
     *            tells if the data should be loaded from the mesh structure
     * @throws BlenderFileException
     *             an exception is thrown when problems with file reading occur
     */
    protected TemporalMesh(Structure meshStructure, BlenderContext blenderContext, boolean loadData) throws BlenderFileException {
        this.blenderContext = blenderContext;
        this.meshStructure = meshStructure;

        if (loadData) {
            name = meshStructure.getName();

            MeshHelper meshHelper = blenderContext.getHelper(MeshHelper.class);

            meshHelper.loadVerticesAndNormals(meshStructure, vertices, normals);
            verticesColors = meshHelper.loadVerticesColors(meshStructure, blenderContext);
            LinkedHashMap<String, List<Vector2f>> userUVGroups = meshHelper.loadUVCoordinates(meshStructure);
            vertexGroups = meshHelper.loadVerticesGroups(meshStructure);

            faces = Face.loadAll(meshStructure, userUVGroups, verticesColors, this, blenderContext);
            edges = Edge.loadAll(meshStructure, this);
            points = Point.loadAll(meshStructure);

            this.rebuildIndexesMappings();
        }
    }

    /**
     * @return the blender context
     */
    public BlenderContext getBlenderContext() {
        return blenderContext;
    }

    /**
     * @return the vertices of the mesh
     */
    public List<Vector3f> getVertices() {
        return vertices;
    }

    /**
     * @return the normals of the mesh
     */
    public List<Vector3f> getNormals() {
        return normals;
    }

    /**
     * @return all faces
     */
    public List<Face> getFaces() {
        return faces;
    }

    /**
     * @return all edges
     */
    public List<Edge> getEdges() {
        return edges;
    }

    /**
     * @return all points (do not mistake it with vertices)
     */
    public List<Point> getPoints() {
        return points;
    }

    /**
     * @return all vertices colors
     */
    public List<byte[]> getVerticesColors() {
        return verticesColors;
    }

    /**
     * @return all vertex groups for the vertices (each map has groups for the proper vertex)
     */
    public List<Map<String, Float>> getVertexGroups() {
        return vertexGroups;
    }

    /**
     * @return the faces that contain the given index or null if none contain it
     */
    public Collection<Face> getAdjacentFaces(Integer index) {
        return indexToFaceMapping.get(index);
    }

    /**
     * @param the
     *            edge of the mesh
     * @return a list of faces that contain the given edge or an empty list
     */
    public Collection<Face> getAdjacentFaces(Edge edge) {
        List<Face> result = new ArrayList<Face>(indexToFaceMapping.get(edge.getFirstIndex()));
        Set<Face> secondIndexAdjacentFaces = indexToFaceMapping.get(edge.getSecondIndex());
        if (secondIndexAdjacentFaces != null) {
            result.retainAll(indexToFaceMapping.get(edge.getSecondIndex()));
        }
        return result;
    }

    /**
     * @param the
     *            index of the mesh
     * @return a list of edges that contain the index
     */
    public Collection<Edge> getAdjacentEdges(Integer index) {
        return indexToEdgeMapping.get(index);
    }

    /**
     * Tells if the given edge is a boundary edge. The boundary edge means that it belongs to a single
     * face or to none.
     * @param the
     *            edge of the mesh
     * @return <b>true</b> if the edge is a boundary one and <b>false</b> otherwise
     */
    public boolean isBoundary(Edge edge) {
        return this.getAdjacentFaces(edge).size() <= 1;
    }

    /**
     * The method tells if the given index is a boundary index. A boundary index belongs to at least
     * one boundary edge.
     * @param index
     *            the index of the mesh
     * @return <b>true</b> if the index is a boundary one and <b>false</b> otherwise
     */
    public boolean isBoundary(Integer index) {
        Collection<Edge> adjacentEdges = this.getAdjacentEdges(index);
        for (Edge edge : adjacentEdges) {
            if (this.isBoundary(edge)) {
                return true;
            }
        }
        return false;
    }

    @Override
    public TemporalMesh clone() {
        try {
            TemporalMesh result = new TemporalMesh(meshStructure, blenderContext, false);
            result.name = name;
            for (Vector3f v : vertices) {
                result.vertices.add(v.clone());
            }
            for (Vector3f n : normals) {
                result.normals.add(n.clone());
            }
            for (Map<String, Float> group : vertexGroups) {
                result.vertexGroups.add(new HashMap<String, Float>(group));
            }
            for (byte[] vertColor : verticesColors) {
                result.verticesColors.add(vertColor.clone());
            }
            result.materials = materials;
            result.properties = properties;
            result.boneIndexes.putAll(boneIndexes);
            result.postMeshCreationModifiers.addAll(postMeshCreationModifiers);
            for (Face face : faces) {
                result.faces.add(face.clone());
            }
            for (Edge edge : edges) {
                result.edges.add(edge.clone());
            }
            for (Point point : points) {
                result.points.add(point.clone());
            }
            result.rebuildIndexesMappings();
            return result;
        } catch (BlenderFileException e) {
            LOGGER.log(Level.SEVERE, "Error while cloning the temporal mesh: {0}. Returning null.", e.getLocalizedMessage());
        }
        return null;
    }

    /**
     * The method rebuilds the mappings between faces and edges. Should be called after
     * every major change of the temporal mesh done outside it.
     * @note I will remove this method soon and make the mappings to be done automatically
     *       when the mesh is modified.
     */
    public void rebuildIndexesMappings() {
        indexToEdgeMapping.clear();
        indexToFaceMapping.clear();
        for (Face face : faces) {
            for (Integer index : face.getIndexes()) {
                Set<Face> faces = indexToFaceMapping.get(index);
                if (faces == null) {
                    faces = new HashSet<Face>();
                    indexToFaceMapping.put(index, faces);
                }
                faces.add(face);
            }
        }
        for (Edge edge : edges) {
            Set<Edge> edges = indexToEdgeMapping.get(edge.getFirstIndex());
            if (edges == null) {
                edges = new HashSet<Edge>();
                indexToEdgeMapping.put(edge.getFirstIndex(), edges);
            }
            edges.add(edge);
            edges = indexToEdgeMapping.get(edge.getSecondIndex());
            if (edges == null) {
                edges = new HashSet<Edge>();
                indexToEdgeMapping.put(edge.getSecondIndex(), edges);
            }
            edges.add(edge);
        }
    }

    @Override
    public void updateModelBound() {
        if (boundingBox == null) {
            boundingBox = new BoundingBox();
        }
        Vector3f min = new Vector3f(Float.MAX_VALUE, Float.MAX_VALUE, Float.MAX_VALUE);
        Vector3f max = new Vector3f(Float.MIN_VALUE, Float.MIN_VALUE, Float.MIN_VALUE);
        for (Vector3f v : vertices) {
            min.set(Math.min(min.x, v.x), Math.min(min.y, v.y), Math.min(min.z, v.z));
            max.set(Math.max(max.x, v.x), Math.max(max.y, v.y), Math.max(max.z, v.z));
        }
        boundingBox.setMinMax(min, max);
    }

    @Override
    public BoundingVolume getModelBound() {
        this.updateModelBound();
        return boundingBox;
    }

    @Override
    public BoundingVolume getWorldBound() {
        this.updateModelBound();
        Node parent = this.getParent();
        if (parent != null) {
            BoundingVolume bv = boundingBox.clone();
            bv.setCenter(parent.getWorldTranslation());
            return bv;
        } else {
            return boundingBox;
        }
    }

    /**
     * Triangulates the mesh.
     */
    public void triangulate() {
        LOGGER.fine("Triangulating temporal mesh.");
        for (Face face : faces) {
            face.triangulate();
        }
    }

    /**
     * The method appends the given mesh to the current one. New faces and vertices and indexes are added.
     * @param mesh
     *            the mesh to be appended
     */
    public void append(TemporalMesh mesh) {
        if (mesh != null) {
            // we need to shift the indexes in faces, lines and points
            int shift = vertices.size();
            if (shift > 0) {
                for (Face face : mesh.faces) {
                    face.getIndexes().shiftIndexes(shift, null);
                    face.setTemporalMesh(this);
                }
                for (Edge edge : mesh.edges) {
                    edge.shiftIndexes(shift, null);
                }
                for (Point point : mesh.points) {
                    point.shiftIndexes(shift, null);
                }
            }

            faces.addAll(mesh.faces);
            edges.addAll(mesh.edges);
            points.addAll(mesh.points);

            vertices.addAll(mesh.vertices);
            normals.addAll(mesh.normals);
            vertexGroups.addAll(mesh.vertexGroups);
            verticesColors.addAll(mesh.verticesColors);
            boneIndexes.putAll(mesh.boneIndexes);

            this.rebuildIndexesMappings();
        }
    }

    /**
     * Sets the properties of the mesh.
     * @param properties
     *            the properties of the mesh
     */
    public void setProperties(Properties properties) {
        this.properties = properties;
    }

    /**
     * Sets the materials of the mesh.
     * @param materials
     *            the materials of the mesh
     */
    public void setMaterials(MaterialContext[] materials) {
        this.materials = materials;
    }

    /**
     * Adds bone index to the mesh.
     * @param boneName
     *            the name of the bone
     * @param boneIndex
     *            the index of the bone
     */
    public void addBoneIndex(String boneName, Integer boneIndex) {
        boneIndexes.put(boneName, boneIndex);
    }

    /**
     * The modifier to be applied after the geometries are created.
     * @param modifier
     *            the modifier to be applied
     */
    public void applyAfterMeshCreate(Modifier modifier) {
        postMeshCreationModifiers.add(modifier);
    }

    @Override
    public int getVertexCount() {
        return vertices.size();
    }

    /**
     * Removes all vertices from the mesh.
     */
    public void clear() {
        vertices.clear();
        normals.clear();
        vertexGroups.clear();
        verticesColors.clear();
        faces.clear();
        edges.clear();
        points.clear();
        indexToEdgeMapping.clear();
        indexToFaceMapping.clear();
    }

    /**
     * The mesh builds geometries from the mesh. The result is stored in the blender context
     * under the mesh's OMA.
     */
    public void toGeometries() {
        LOGGER.log(Level.FINE, "Converting temporal mesh {0} to jme geometries.", name);
        List<Geometry> result = new ArrayList<Geometry>();
        MeshHelper meshHelper = blenderContext.getHelper(MeshHelper.class);
        Node parent = this.getParent();
        parent.detachChild(this);

        this.prepareFacesGeometry(result, meshHelper);
        this.prepareLinesGeometry(result, meshHelper);
        this.preparePointsGeometry(result, meshHelper);

        blenderContext.addLoadedFeatures(meshStructure.getOldMemoryAddress(), LoadedDataType.FEATURE, result);

        for (Geometry geometry : result) {
            parent.attachChild(geometry);
        }

        for (Modifier modifier : postMeshCreationModifiers) {
            modifier.postMeshCreationApply(parent, blenderContext);
        }
    }

    /**
     * The method creates geometries from faces.
     * @param result
     *            the list where new geometries will be appended
     * @param meshHelper
     *            the mesh helper
     */
    protected void prepareFacesGeometry(List<Geometry> result, MeshHelper meshHelper) {
        LOGGER.fine("Preparing faces geometries.");
        this.triangulate();

        Vector3f[] tempVerts = new Vector3f[3];
        Vector3f[] tempNormals = new Vector3f[3];
        byte[][] tempVertColors = new byte[3][];
        List<Map<Float, Integer>> boneBuffers = new ArrayList<Map<Float, Integer>>(3);

        LOGGER.log(Level.FINE, "Appending {0} faces to mesh buffers.", faces.size());
        Map<Integer, MeshBuffers> faceMeshes = new HashMap<Integer, MeshBuffers>();
        for (Face face : faces) {
            MeshBuffers meshBuffers = faceMeshes.get(face.getMaterialNumber());
            if (meshBuffers == null) {
                meshBuffers = new MeshBuffers(face.getMaterialNumber());
                faceMeshes.put(face.getMaterialNumber(), meshBuffers);
            }

            List<List<Integer>> triangulatedIndexes = face.getCurrentIndexes();
            List<byte[]> vertexColors = face.getVertexColors();

            for (List<Integer> indexes : triangulatedIndexes) {
                assert indexes.size() == 3 : "The mesh has not been properly triangulated!";
                
                Vector3f normal = null;
                if(!face.isSmooth()) {
                    normal = FastMath.computeNormal(vertices.get(indexes.get(0)), vertices.get(indexes.get(1)), vertices.get(indexes.get(2)));
                }
                
                boneBuffers.clear();
                for (int i = 0; i < 3; ++i) {
                    int vertIndex = indexes.get(i);
                    tempVerts[i] = vertices.get(vertIndex);
                    tempNormals[i] = normal != null ? normal : normals.get(vertIndex);
                    tempVertColors[i] = vertexColors != null ? vertexColors.get(face.getIndexes().indexOf(vertIndex)) : null;

                    if (boneIndexes.size() > 0 && vertexGroups.size() > 0) {
                        Map<Float, Integer> boneBuffersForVertex = new HashMap<Float, Integer>();
                        Map<String, Float> vertexGroupsForVertex = vertexGroups.get(vertIndex);
                        for (Entry<String, Integer> entry : boneIndexes.entrySet()) {
                            if (vertexGroupsForVertex.containsKey(entry.getKey())) {
                                float weight = vertexGroupsForVertex.get(entry.getKey());
                                if (weight > MINIMUM_BONE_WEIGHT) {
                                	// only values of weight greater than MINIMUM_BONE_WEIGHT are used
                                	// if all non zero weights were used, and they were samm enough, problems with normalisation would occur
                                	// because adding a very small value to 1.0 will give 1.0
                                	// so in order to avoid such errors, which can cause severe animation artifacts we need to use some minimum weight value
                                    boneBuffersForVertex.put(weight, entry.getValue());
                                }
                            }
                        }
                        if (boneBuffersForVertex.size() == 0) {// attach the vertex to zero-indexed bone so that it does not collapse to (0, 0, 0)
                            boneBuffersForVertex.put(1.0f, 0);
                        }
                        boneBuffers.add(boneBuffersForVertex);
                    }
                }

                Map<String, List<Vector2f>> uvs = meshHelper.selectUVSubset(face, indexes.toArray(new Integer[indexes.size()]));
                meshBuffers.append(face.isSmooth(), tempVerts, tempNormals, uvs, tempVertColors, boneBuffers);
            }
        }

        LOGGER.fine("Converting mesh buffers to geometries.");
        Map<Geometry, MeshBuffers> geometryToBuffersMap = new HashMap<Geometry, MeshBuffers>();
        for (Entry<Integer, MeshBuffers> entry : faceMeshes.entrySet()) {
            MeshBuffers meshBuffers = entry.getValue();

            Mesh mesh = new Mesh();

            if (meshBuffers.isShortIndexBuffer()) {
                mesh.setBuffer(Type.Index, 1, (ShortBuffer) meshBuffers.getIndexBuffer());
            } else {
                mesh.setBuffer(Type.Index, 1, (IntBuffer) meshBuffers.getIndexBuffer());
            }
            mesh.setBuffer(meshBuffers.getPositionsBuffer());
            mesh.setBuffer(meshBuffers.getNormalsBuffer());
            if (meshBuffers.areVertexColorsUsed()) {
                mesh.setBuffer(Type.Color, 4, meshBuffers.getVertexColorsBuffer());
                mesh.getBuffer(Type.Color).setNormalized(true);
            }

            BoneBuffersData boneBuffersData = meshBuffers.getBoneBuffers();
            if (boneBuffersData != null) {
                mesh.setMaxNumWeights(boneBuffersData.maximumWeightsPerVertex);
                mesh.setBuffer(boneBuffersData.verticesWeights);
                mesh.setBuffer(boneBuffersData.verticesWeightsIndices);

                LOGGER.fine("Generating bind pose and normal buffers.");
                mesh.generateBindPose(true);

                // change the usage type of vertex and normal buffers from Static to Stream
                mesh.getBuffer(Type.Position).setUsage(Usage.Stream);
                mesh.getBuffer(Type.Normal).setUsage(Usage.Stream);

                // creating empty buffers for HW skinning; the buffers will be setup if ever used
                VertexBuffer verticesWeightsHW = new VertexBuffer(Type.HWBoneWeight);
                VertexBuffer verticesWeightsIndicesHW = new VertexBuffer(Type.HWBoneIndex);
                mesh.setBuffer(verticesWeightsHW);
                mesh.setBuffer(verticesWeightsIndicesHW);
            }

            Geometry geometry = new Geometry(name + (result.size() + 1), mesh);
            if (properties != null && properties.getValue() != null) {
                meshHelper.applyProperties(geometry, properties);
            }
            result.add(geometry);

            geometryToBuffersMap.put(geometry, meshBuffers);
        }

        LOGGER.fine("Applying materials to geometries.");
        for (Entry<Geometry, MeshBuffers> entry : geometryToBuffersMap.entrySet()) {
            int materialIndex = entry.getValue().getMaterialIndex();
            Geometry geometry = entry.getKey();
            if (materialIndex >= 0 && materials != null && materials.length > materialIndex && materials[materialIndex] != null) {
                materials[materialIndex].applyMaterial(geometry, meshStructure.getOldMemoryAddress(), entry.getValue().getUvCoords(), blenderContext);
            } else {
                Material defaultMaterial = blenderContext.getDefaultMaterial().clone();
                defaultMaterial.getAdditionalRenderState().setFaceCullMode(FaceCullMode.Off);
                geometry.setMaterial(defaultMaterial);
            }
        }
    }

    /**
     * The method creates geometries from lines.
     * @param result
     *            the list where new geometries will be appended
     * @param meshHelper
     *            the mesh helper
     */
    protected void prepareLinesGeometry(List<Geometry> result, MeshHelper meshHelper) {
        if (edges.size() > 0) {
            LOGGER.fine("Preparing lines geometries.");

            List<List<Integer>> separateEdges = new ArrayList<List<Integer>>();
            List<Edge> edges = new ArrayList<Edge>(this.edges.size());
            for (Edge edge : this.edges) {
                if (!edge.isInFace()) {
                    edges.add(edge);
                }
            }
            while (edges.size() > 0) {
                boolean edgeAppended = false;
                int edgeIndex = 0;
                for (List<Integer> list : separateEdges) {
                    for (edgeIndex = 0; edgeIndex < edges.size() && !edgeAppended; ++edgeIndex) {
                        Edge edge = edges.get(edgeIndex);
                        if (list.get(0).equals(edge.getFirstIndex())) {
                            list.add(0, edge.getSecondIndex());
                            --edgeIndex;
                            edgeAppended = true;
                        } else if (list.get(0).equals(edge.getSecondIndex())) {
                            list.add(0, edge.getFirstIndex());
                            --edgeIndex;
                            edgeAppended = true;
                        } else if (list.get(list.size() - 1).equals(edge.getFirstIndex())) {
                            list.add(edge.getSecondIndex());
                            --edgeIndex;
                            edgeAppended = true;
                        } else if (list.get(list.size() - 1).equals(edge.getSecondIndex())) {
                            list.add(edge.getFirstIndex());
                            --edgeIndex;
                            edgeAppended = true;
                        }
                    }
                    if (edgeAppended) {
                        break;
                    }
                }
                Edge edge = edges.remove(edgeAppended ? edgeIndex : 0);
                if (!edgeAppended) {
                    separateEdges.add(new ArrayList<Integer>(Arrays.asList(edge.getFirstIndex(), edge.getSecondIndex())));
                }
            }

            for (List<Integer> list : separateEdges) {
                MeshBuffers meshBuffers = new MeshBuffers(0);
                for (int index : list) {
                    meshBuffers.append(vertices.get(index), normals.get(index));
                }
                Mesh mesh = new Mesh();
                mesh.setLineWidth(blenderContext.getBlenderKey().getLinesWidth());
                mesh.setMode(Mode.LineStrip);
                if (meshBuffers.isShortIndexBuffer()) {
                    mesh.setBuffer(Type.Index, 1, (ShortBuffer) meshBuffers.getIndexBuffer());
                } else {
                    mesh.setBuffer(Type.Index, 1, (IntBuffer) meshBuffers.getIndexBuffer());
                }
                mesh.setBuffer(meshBuffers.getPositionsBuffer());
                mesh.setBuffer(meshBuffers.getNormalsBuffer());

                Geometry geometry = new Geometry(meshStructure.getName() + (result.size() + 1), mesh);
                geometry.setMaterial(meshHelper.getBlackUnshadedMaterial(blenderContext));
                if (properties != null && properties.getValue() != null) {
                    meshHelper.applyProperties(geometry, properties);
                }
                result.add(geometry);
            }
        }
    }

    /**
     * The method creates geometries from points.
     * @param result
     *            the list where new geometries will be appended
     * @param meshHelper
     *            the mesh helper
     */
    protected void preparePointsGeometry(List<Geometry> result, MeshHelper meshHelper) {
        if (points.size() > 0) {
            LOGGER.fine("Preparing point geometries.");

            MeshBuffers pointBuffers = new MeshBuffers(0);
            for (Point point : points) {
                pointBuffers.append(vertices.get(point.getIndex()), normals.get(point.getIndex()));
            }
            Mesh pointsMesh = new Mesh();
            pointsMesh.setMode(Mode.Points);
            pointsMesh.setPointSize(blenderContext.getBlenderKey().getPointsSize());
            if (pointBuffers.isShortIndexBuffer()) {
                pointsMesh.setBuffer(Type.Index, 1, (ShortBuffer) pointBuffers.getIndexBuffer());
            } else {
                pointsMesh.setBuffer(Type.Index, 1, (IntBuffer) pointBuffers.getIndexBuffer());
            }
            pointsMesh.setBuffer(pointBuffers.getPositionsBuffer());
            pointsMesh.setBuffer(pointBuffers.getNormalsBuffer());

            Geometry pointsGeometry = new Geometry(meshStructure.getName() + (result.size() + 1), pointsMesh);
            pointsGeometry.setMaterial(meshHelper.getBlackUnshadedMaterial(blenderContext));
            if (properties != null && properties.getValue() != null) {
                meshHelper.applyProperties(pointsGeometry, properties);
            }
            result.add(pointsGeometry);
        }
    }

    @Override
    public String toString() {
        return "TemporalMesh [name=" + name + ", vertices.size()=" + vertices.size() + "]";
    }

    @Override
    public int hashCode() {
        final int prime = 31;
        int result = 1;
        result = prime * result + (meshStructure == null ? 0 : meshStructure.hashCode());
        return result;
    }

    @Override
    public boolean equals(Object obj) {
        if (this == obj) {
            return true;
        }
        if (!(obj instanceof TemporalMesh)) {
            return false;
        }
        TemporalMesh other = (TemporalMesh) obj;
        return meshStructure.getOldMemoryAddress().equals(other.meshStructure.getOldMemoryAddress());
    }
}
