/*
 * Copyright (c) 2009-2013 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 class is the java implementation of
 * the enhanced version of Ogre engine Lod generator, by Péter Szücs, originally
 * based on Stan Melax "easy mesh simplification". The MIT licenced C++ source
 * code can be found here
 * https://github.com/worldforge/ember/tree/master/src/components/ogre/lod
 * The licencing for the original code is : 
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * 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.
 */
package jme3tools.optimize;

import com.jme3.bounding.BoundingSphere;
import com.jme3.math.Vector3f;
import com.jme3.scene.Geometry;
import com.jme3.scene.Mesh;
import com.jme3.scene.VertexBuffer;
import com.jme3.util.BufferUtils;
import java.nio.Buffer;
import java.nio.FloatBuffer;
import java.nio.IntBuffer;
import java.nio.ShortBuffer;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
import java.util.SortedSet;
import java.util.TreeSet;
import java.util.logging.Level;
import java.util.logging.Logger;

/**
 * This is an utility class that allows to generated the lod levels for an
 * arbitrary mesh. It computes a collapse cost for each vertex and each edges.
 * The higher the cost the most likely collapsing the edge or the vertex will
 * produce artifacts on the mesh. <p>This class is the java implementation of
 * the enhanced version of Ogre engine Lod generator, by Péter Szücs, originally
 * based on Stan Melax "easy mesh simplification". The MIT licenced C++ source
 * code can be found here
 * https://github.com/worldforge/ember/tree/master/src/components/ogre/lod more
 * informations can be found here http://www.melax.com/polychop
 * http://sajty.elementfx.com/progressivemesh/GSoC2012.pdf </p>
 *
 * <p>The algorithm sort the vertice according to their collapsse cost
 * ascending. It collapse from the "cheapest" vertex to the more expensive.<br>
 * <strong>Usage : </strong><br>
 * <pre>
 *      LodGenerator lODGenerator = new LodGenerator(geometry);
 *      lODGenerator.bakeLods(reductionMethod,reductionvalue);
 * </pre> redutionMethod type is VertexReductionMethod described here
 * {@link TriangleReductionMethod} reductionvalue depends on the
 * reductionMethod<p>
 *
 *
 * @author Nehon
 */
public class LodGenerator {
    
    private static final Logger logger = Logger.getLogger(LodGenerator.class.getName());
    private static final float NEVER_COLLAPSE_COST = Float.MAX_VALUE;
    private static final float UNINITIALIZED_COLLAPSE_COST = Float.POSITIVE_INFINITY;
    private Vector3f tmpV1 = new Vector3f();
    private Vector3f tmpV2 = new Vector3f();
    private boolean bestQuality = true;
    private int indexCount = 0;
    private List<Vertex> collapseCostSet = new ArrayList<Vertex>();
    private float collapseCostLimit;
    private List<Triangle> triangleList;
    private List<Vertex> vertexList = new ArrayList<Vertex>();
    private float meshBoundingSphereRadius;
    private Mesh mesh;

    /**
     * Describe the way trinagles will be removed. <br> PROPORTIONAL :
     * Percentage of triangles to be removed from the mesh. Valid range is a
     * number between 0.0 and 1.0 <br> CONSTANT : Triangle count to be removed
     * from the mesh. Pass only integers or it will be rounded. <br>
     * COLLAPSE_COST : Reduces the vertices, until the cost is bigger then the
     * given value. Collapse cost is equal to the amount of artifact the
     * reduction causes. This generates the best Lod output, but the collapse
     * cost depends on implementation.
     */
    public enum TriangleReductionMethod {

        /**
         * Percentage of triangles to be removed from the mesh.
         *
         * Valid range is a number between 0.0 and 1.0
         */
        PROPORTIONAL,
        /**
         * Triangle count to be removed from the mesh.
         *
         * Pass only integers or it will be rounded.
         */
        CONSTANT,
        /**
         * Reduces the vertices, until the cost is bigger then the given value.
         *
         * Collapse cost is equal to the amount of artifact the reduction
         * causes. This generates the best Lod output, but the collapse cost
         * depends on implementation.
         */
        COLLAPSE_COST
    };
    
    private class Edge {
        
        Vertex destination;
        float collapseCost = UNINITIALIZED_COLLAPSE_COST;
        int refCount;
        
        public Edge(Vertex destination) {
            this.destination = destination;
        }
        
        public void set(Edge other) {
            destination = other.destination;
            collapseCost = other.collapseCost;
            refCount = other.refCount;
        }
        
        @Override
        public boolean equals(Object obj) {
            if (!(obj instanceof Edge)) {
                return false;
            }
            return destination == ((Edge) obj).destination;
        }
        
        @Override
        public int hashCode() {
            return destination.hashCode();
        }
        
        @Override
        public String toString() {
            return "Edge{" + "collapsTo " + destination.index + '}';
        }
    }
    
    private class Vertex {
        
        Vector3f position = new Vector3f();
        float collapseCost = UNINITIALIZED_COLLAPSE_COST;
        List<Edge> edges = new ArrayList<Edge>();
        Set<Triangle> triangles = new HashSet<Triangle>();
        Vertex collapseTo;
        boolean isSeam;
        int index;//index in the buffer for debugging

        @Override
        public String toString() {
            return index + " : " + position.toString();
        }
    }
    
    private class Triangle {
        
        Vertex[] vertex = new Vertex[3];
        Vector3f normal;
        boolean isRemoved;
        //indices of the vertices in the vertex buffer
        int[] vertexId = new int[3];
        
        void computeNormal() {
            // Cross-product 2 edges
            tmpV1.set(vertex[1].position).subtractLocal(vertex[0].position);
            tmpV2.set(vertex[2].position).subtractLocal(vertex[1].position);
            
            normal = tmpV1.cross(tmpV2);
            normal.normalizeLocal();
        }
        
        boolean hasVertex(Vertex v) {
            return (v == vertex[0] || v == vertex[1] || v == vertex[2]);
        }
        
        int getVertexIndex(Vertex v) {
            for (int i = 0; i < 3; i++) {
                if (vertex[i] == v) {
                    return vertexId[i];
                }
            }
            throw new IllegalArgumentException("Vertex " + v + "is not part of triangle" + this);
        }
        
        boolean isMalformed() {
            return vertex[0] == vertex[1] || vertex[0] == vertex[2] || vertex[1] == vertex[2];
        }
        
        @Override
        public String toString() {
            String out = "Triangle{\n";
            for (int i = 0; i < 3; i++) {
                out += vertexId[i] + " : " + vertex[i].toString() + "\n";
            }
            out += '}';
            return out;
        }
    }
    /**
     * Compartator used to sort vertices according to their collapse cost
     */
    private Comparator collapseComparator = new Comparator<Vertex>() {
        public int compare(Vertex o1, Vertex o2) {
            if (Float.compare(o1.collapseCost, o2.collapseCost) == 0) {
                return 0;
            }
            if (o1.collapseCost < o2.collapseCost) {
                return -1;
            }
            return 1;
        }
    };

    /**
     * Construct a LodGenerator for the given geometry
     *
     * @param geom the geometry to consider to generate de Lods.
     */
    public LodGenerator(Geometry geom) {
        mesh = geom.getMesh();
        build();
    }
    
    private void build() {
        BoundingSphere bs = new BoundingSphere();
        bs.computeFromPoints(mesh.getFloatBuffer(VertexBuffer.Type.Position));
        meshBoundingSphereRadius = bs.getRadius();
        List<Vertex> vertexLookup = new ArrayList<Vertex>();
        initialize();
        
        gatherVertexData(mesh, vertexLookup);
        gatherIndexData(mesh, vertexLookup);
        computeCosts();
       // assert (assertValidMesh());
        
    }
    
    private void gatherVertexData(Mesh mesh, List<Vertex> vertexLookup) {

        //in case the model is currently animating with software animation
        //attempting to retrieve the bind position instead of the position.
        VertexBuffer position = mesh.getBuffer(VertexBuffer.Type.BindPosePosition);
        if (position == null) {
            position = mesh.getBuffer(VertexBuffer.Type.Position);
        }
        FloatBuffer pos = (FloatBuffer) position.getDataReadOnly();
        pos.rewind();
        
        while (pos.remaining() != 0) {
            Vertex v = new Vertex();
            v.position.setX(pos.get());
            v.position.setY(pos.get());
            v.position.setZ(pos.get());
            v.isSeam = false;
            Vertex existingV = findSimilar(v);
            if (existingV != null) {
                //vertex position already exists
                existingV.isSeam = true;
                v.isSeam = true;
            } else {
                vertexList.add(v);
            }
            vertexLookup.add(v);
        }
        pos.rewind();
    }
    
    private Vertex findSimilar(Vertex v) {
        for (Vertex vertex : vertexList) {
            if (vertex.position.equals(v.position)) {
                return vertex;
            }
        }
        return null;
    }
    
    private void gatherIndexData(Mesh mesh, List<Vertex> vertexLookup) {
        VertexBuffer indexBuffer = mesh.getBuffer(VertexBuffer.Type.Index);
        indexCount = indexBuffer.getNumElements() * 3;
        Buffer b = indexBuffer.getDataReadOnly();
        b.rewind();
        
        while (b.remaining() != 0) {
            Triangle tri = new Triangle();
            tri.isRemoved = false;
            triangleList.add(tri);            
            for (int i = 0; i < 3; i++) {
                if (b instanceof IntBuffer) {
                    tri.vertexId[i] = ((IntBuffer) b).get();
                } else {
                    //bit shift to avoid negative values due to conversion form short to int.
                    //we need an unsigned int here.
                    tri.vertexId[i] = ((ShortBuffer) b).get()& 0xffff;
                }
               // assert (tri.vertexId[i] < vertexLookup.size());
                tri.vertex[i] = vertexLookup.get(tri.vertexId[i]);
                //debug only;
                tri.vertex[i].index = tri.vertexId[i];
            }
            if (tri.isMalformed()) {
                if (!tri.isRemoved) {
                    logger.log(Level.FINE, "malformed triangle found with ID:{0}\n{1} It will be excluded from Lod level calculations.", new Object[]{triangleList.indexOf(tri), tri.toString()});
                    tri.isRemoved = true;
                    indexCount -= 3;
                }
                
            } else {
                tri.computeNormal();
                addTriangleToEdges(tri);
            }
        }
        b.rewind();
    }
    
    private void computeCosts() {
        collapseCostSet.clear();
        
        for (Vertex vertex : vertexList) {
            
            if (!vertex.edges.isEmpty()) {
                computeVertexCollapseCost(vertex);
            } else {
                logger.log(Level.FINE, "Found isolated vertex {0} It will be excluded from Lod level calculations.", vertex);
            }
        }
//        assert (vertexList.size() == collapseCostSet.size());
//        assert (checkCosts());
    }

    //Debug only
    private boolean checkCosts() {
        for (Vertex vertex : vertexList) {
            boolean test = find(collapseCostSet, vertex);
            if (!test) {
                System.out.println("vertex " + vertex.index + " not present in collapse costs");
                return false;
            }
        }
        return true;
    }
    
    private void computeVertexCollapseCost(Vertex vertex) {
        
        vertex.collapseCost = UNINITIALIZED_COLLAPSE_COST;
      //  assert (!vertex.edges.isEmpty());
        for (Edge edge : vertex.edges) {
            edge.collapseCost = computeEdgeCollapseCost(vertex, edge);
         //   assert (edge.collapseCost != UNINITIALIZED_COLLAPSE_COST);
            if (vertex.collapseCost > edge.collapseCost) {
                vertex.collapseCost = edge.collapseCost;
                vertex.collapseTo = edge.destination;
            }
        }
       // assert (vertex.collapseCost != UNINITIALIZED_COLLAPSE_COST);
        collapseCostSet.add(vertex);
    }
    
    float computeEdgeCollapseCost(Vertex src, Edge dstEdge) {
        // This is based on Ogre's collapse cost calculation algorithm.

        Vertex dest = dstEdge.destination;

        // Check for singular triangle destruction
        // If src and dest both only have 1 triangle (and it must be a shared one)
        // then this would destroy the shape, so don't do this
        if (src.triangles.size() == 1 && dest.triangles.size() == 1) {
            return NEVER_COLLAPSE_COST;
        }

        // Degenerate case check
        // Are we going to invert a face normal of one of the neighbouring faces?
        // Can occur when we have a very small remaining edge and collapse crosses it
        // Look for a face normal changing by > 90 degrees
        for (Triangle triangle : src.triangles) {
            // Ignore the deleted faces (those including src & dest)
            if (!triangle.hasVertex(dest)) {
                // Test the new face normal
                Vertex pv0, pv1, pv2;

                // Replace src with dest wherever it is
                pv0 = (triangle.vertex[0] == src) ? dest : triangle.vertex[0];
                pv1 = (triangle.vertex[1] == src) ? dest : triangle.vertex[1];
                pv2 = (triangle.vertex[2] == src) ? dest : triangle.vertex[2];

                // Cross-product 2 edges
                tmpV1.set(pv1.position).subtractLocal(pv0.position);
                tmpV2.set(pv2.position).subtractLocal(pv1.position);

                //computing the normal
                Vector3f newNormal = tmpV1.crossLocal(tmpV2);
                newNormal.normalizeLocal();

                // Dot old and new face normal
                // If < 0 then more than 90 degree difference
                if (newNormal.dot(triangle.normal) < 0.0f) {
                    // Don't do it!
                    return NEVER_COLLAPSE_COST;
                }
            }
        }
        
        float cost;

        // Special cases
        // If we're looking at a border vertex
        if (isBorderVertex(src)) {
            if (dstEdge.refCount > 1) {
                // src is on a border, but the src-dest edge has more than one tri on it
                // So it must be collapsing inwards
                // Mark as very high-value cost
                // curvature = 1.0f;
                cost = 1.0f;
            } else {
                // Collapsing ALONG a border
                // We can't use curvature to measure the effect on the model
                // Instead, see what effect it has on 'pulling' the other border edges
                // The more colinear, the less effect it will have
                // So measure the 'kinkiness' (for want of a better term)

                // Find the only triangle using this edge.
                // PMTriangle* triangle = findSideTriangle(src, dst);

                cost = 0.0f;
                Vector3f collapseEdge = tmpV1.set(src.position).subtractLocal(dest.position);
                collapseEdge.normalizeLocal();
                
                for (Edge edge : src.edges) {
                    
                    Vertex neighbor = edge.destination;
                    //reference check intended
                    if (neighbor != dest && edge.refCount == 1) {
                        Vector3f otherBorderEdge = tmpV2.set(src.position).subtractLocal(neighbor.position);
                        otherBorderEdge.normalizeLocal();
                        // This time, the nearer the dot is to -1, the better, because that means
                        // the edges are opposite each other, therefore less kinkiness
                        // Scale into [0..1]
                        float kinkiness = (otherBorderEdge.dot(collapseEdge) + 1.002f) * 0.5f;
                        cost = Math.max(cost, kinkiness);
                    }
                }
            }
        } else { // not a border

            // Standard inner vertex
            // Calculate curvature
            // use the triangle facing most away from the sides
            // to determine our curvature term
            // Iterate over src's faces again
            cost = 0.001f;
            
            for (Triangle triangle : src.triangles) {
                float mincurv = 1.0f; // curve for face i and closer side to it

                for (Triangle triangle2 : src.triangles) {
                    if (triangle2.hasVertex(dest)) {

                        // Dot product of face normal gives a good delta angle
                        float dotprod = triangle.normal.dot(triangle2.normal);
                        // NB we do (1-..) to invert curvature where 1 is high curvature [0..1]
                        // Whilst dot product is high when angle difference is low
                        mincurv = Math.min(mincurv, (1.002f - dotprod) * 0.5f);
                    }
                }
                cost = Math.max(cost, mincurv);
            }
        }

        // check for texture seam ripping
        if (src.isSeam) {
            if (!dest.isSeam) {
                cost += meshBoundingSphereRadius;
            } else {
                cost += meshBoundingSphereRadius * 0.5;
            }
        }
        
     //   assert (cost >= 0);
        
        return cost * src.position.distanceSquared(dest.position);
    }
    int nbCollapsedTri = 0;

    /**
     * Computes the lod and return a list of VertexBuffers that can then be used
     * for lod (use Mesg.setLodLevels(VertexBuffer[]))<br>
     *
     * This method must be fed with the reduction method
     * {@link TriangleReductionMethod} and a list of reduction values.<br> for
     * each value a lod will be generated. <br> The resulting array will always
     * contain at index 0 the original index buffer of the mesh. <p>
     * <strong>Important note :</strong> some meshes cannot be decimated, so the
     * result of this method can varry depending of the given mesh. Also the
     * reduction values are indicative and the produces mesh will not always
     * meet the required reduction.
     *
     * @param reductionMethod the reduction method to use
     * @param reductionValues the reduction value to use for each lod level.
     * @return an array of VertexBuffers containing the different index buffers
     * representing the lod levels.
     */
    public VertexBuffer[] computeLods(TriangleReductionMethod reductionMethod, float... reductionValues) {
        int tricount = triangleList.size();
        int lastBakeVertexCount = tricount;
        int lodCount = reductionValues.length;
        VertexBuffer[] lods = new VertexBuffer[lodCount + 1];
        int numBakedLods = 1;
        lods[0] = mesh.getBuffer(VertexBuffer.Type.Index);
        for (int curLod = 0; curLod < lodCount; curLod++) {
            int neededTriCount = calcLodTriCount(reductionMethod, reductionValues[curLod]);
            while (neededTriCount < tricount) {
                Collections.sort(collapseCostSet, collapseComparator);
                Iterator<Vertex> it = collapseCostSet.iterator();
                
                if (it.hasNext()) {
                    Vertex v = it.next();
                    if (v.collapseCost < collapseCostLimit) {
                        if (!collapse(v)) {
                            logger.log(Level.FINE, "Couldn''t collapse vertex{0}", v.index);
                        }
                        Iterator<Vertex> it2 = collapseCostSet.iterator();
                        if (it2.hasNext()) {
                            it2.next();
                            it2.remove();// Remove src from collapse costs.
                        }
                        
                    } else {
                        break;
                    }
                } else {
                    break;
                }
                tricount = triangleList.size() - nbCollapsedTri;
            }
            logger.log(Level.FINE, "collapsed {0} tris", nbCollapsedTri);
            boolean outSkipped = (lastBakeVertexCount == tricount);
            if (!outSkipped) {
                lastBakeVertexCount = tricount;
                lods[curLod + 1] = makeLod(mesh);
                numBakedLods++;
            }
        }
        if (numBakedLods <= lodCount) {
            VertexBuffer[] bakedLods = new VertexBuffer[numBakedLods];
            System.arraycopy(lods, 0, bakedLods, 0, numBakedLods);
            return bakedLods;
        } else {
            return lods;
        }
    }

    /**
     * Computes the lods and bake them into the mesh<br>
     *
     * This method must be fed with the reduction method
     * {@link TriangleReductionMethod} and a list of reduction values.<br> for
     * each value a lod will be generated. <p> <strong>Important note :</strong>
     * some meshes cannot be decimated, so the result of this method can varry
     * depending of the given mesh. Also the reduction values are indicative and
     * the produces mesh will not always meet the required reduction.
     *
     * @param reductionMethod the reduction method to use
     * @param reductionValues the reduction value to use for each lod level.
     */
    public void bakeLods(TriangleReductionMethod reductionMethod, float... reductionValues) {
        mesh.setLodLevels(computeLods(reductionMethod, reductionValues));
    }
    
    private VertexBuffer makeLod(Mesh mesh) {
        VertexBuffer indexBuffer = mesh.getBuffer(VertexBuffer.Type.Index);
        
        boolean isShortBuffer = indexBuffer.getFormat() == VertexBuffer.Format.UnsignedShort;
        // Create buffers.	
        VertexBuffer lodBuffer = new VertexBuffer(VertexBuffer.Type.Index);
        int bufsize = indexCount == 0 ? 3 : indexCount;
        
        if (isShortBuffer) {
            lodBuffer.setupData(VertexBuffer.Usage.Static, 3, VertexBuffer.Format.UnsignedShort, BufferUtils.createShortBuffer(bufsize));
        } else {
            lodBuffer.setupData(VertexBuffer.Usage.Static, 3, VertexBuffer.Format.UnsignedInt, BufferUtils.createIntBuffer(bufsize));
        }
        
        
        
        lodBuffer.getData().rewind();
        //Check if we should fill it with a "dummy" triangle.
        if (indexCount == 0) {
            if (isShortBuffer) {
                for (int m = 0; m < 3; m++) {
                    ((ShortBuffer) lodBuffer.getData()).put((short) 0);
                }
            } else {
                for (int m = 0; m < 3; m++) {
                    ((IntBuffer) lodBuffer.getData()).put(0);
                }
            }
        }

        // Fill buffers.       
        Buffer buf = lodBuffer.getData();
        buf.rewind();
        for (Triangle triangle : triangleList) {
            if (!triangle.isRemoved) {
            //    assert (indexCount != 0);
                if (isShortBuffer) {
                    for (int m = 0; m < 3; m++) {
                        ((ShortBuffer) buf).put((short) triangle.vertexId[m]);
                        
                    }
                } else {
                    for (int m = 0; m < 3; m++) {
                        ((IntBuffer) buf).put(triangle.vertexId[m]);
                    }
                    
                }
            }
        }
        buf.clear();
        lodBuffer.updateData(buf);
        return lodBuffer;
    }
    
    private int calcLodTriCount(TriangleReductionMethod reductionMethod, float reductionValue) {
        int nbTris = mesh.getTriangleCount();
        switch (reductionMethod) {
            case PROPORTIONAL:
                collapseCostLimit = NEVER_COLLAPSE_COST;
                return (int) (nbTris - (nbTris * (reductionValue)));
            
            case CONSTANT:
                collapseCostLimit = NEVER_COLLAPSE_COST;
                if (reductionValue < nbTris) {
                    return nbTris - (int) reductionValue;
                }
                return 0;
            
            case COLLAPSE_COST:
                collapseCostLimit = reductionValue;
                return 0;
            
            default:
                return nbTris;
        }
    }
    
    private int findDstID(int srcId, List<CollapsedEdge> tmpCollapsedEdges) {
        int i = 0;
        for (CollapsedEdge collapsedEdge : tmpCollapsedEdges) {
            if (collapsedEdge.srcID == srcId) {
                return i;
            }
            i++;
        }
        return Integer.MAX_VALUE;
    }
    
    private class CollapsedEdge {
        
        int srcID;
        int dstID;
    };
    
    private void removeTriangleFromEdges(Triangle triangle, Vertex skip) {
        // skip is needed if we are iterating on the vertex's edges or triangles.
        for (int i = 0; i < 3; i++) {
            if (triangle.vertex[i] != skip) {
                triangle.vertex[i].triangles.remove(triangle);
            }
        }
        for (int i = 0; i < 3; i++) {
            for (int n = 0; n < 3; n++) {
                if (i != n) {
                    removeEdge(triangle.vertex[i], new Edge(triangle.vertex[n]));
                }
            }
        }
    }
    
    private void removeEdge(Vertex v, Edge edge) {
        Edge ed = null;
        for (Edge edge1 : v.edges) {
            if (edge1.equals(edge)) {
                ed = edge1;
                break;
            }
        }
        
        if (ed.refCount == 1) {
            v.edges.remove(ed);
        } else {
            ed.refCount--;
        }
        
    }
    
    boolean isBorderVertex(Vertex vertex) {
        for (Edge edge : vertex.edges) {
            if (edge.refCount == 1) {
                return true;
            }
        }
        return false;
    }
    
    private void addTriangleToEdges(Triangle tri) {
        if (bestQuality) {
            Triangle duplicate = getDuplicate(tri);
            if (duplicate != null) {
                if (!tri.isRemoved) {
                    tri.isRemoved = true;
                    indexCount -= 3;
                    logger.log(Level.FINE, "duplicate triangle found{0}{1} It will be excluded from Lod level calculations.", new Object[]{tri, duplicate});
                }
            }
        }
        for (int i = 0; i < 3; i++) {
            tri.vertex[i].triangles.add(tri);
        }
        for (int i = 0; i < 3; i++) {
            for (int n = 0; n < 3; n++) {
                if (i != n) {
                    addEdge(tri.vertex[i], new Edge(tri.vertex[n]));
                }
            }
        }
    }
    
    private void addEdge(Vertex v, Edge edge) {
      //  assert (edge.destination != v);
        
        for (Edge ed : v.edges) {
            if (ed.equals(edge)) {
                ed.refCount++;
                return;
            }
        }
        
        v.edges.add(edge);
        edge.refCount = 1;
        
    }
    
    private void initialize() {
        triangleList = new ArrayList<LodGenerator.Triangle>();
    }
    
    private Triangle getDuplicate(Triangle triangle) {
        // duplicate triangle detection (where all vertices has the same position)
        for (Triangle tri : triangle.vertex[0].triangles) {
            if (isDuplicateTriangle(triangle, tri)) {
                return tri;
            }
        }
        return null;
    }
    
    private boolean isDuplicateTriangle(Triangle triangle, Triangle triangle2) {
        for (int i = 0; i < 3; i++) {
            if (triangle.vertex[i] != triangle2.vertex[0]
                    || triangle.vertex[i] != triangle2.vertex[1]
                    || triangle.vertex[i] != triangle2.vertex[2]) {
                return false;
            }
        }
        return true;
    }
    
    private void replaceVertexID(Triangle triangle, int oldID, int newID, Vertex dst) {
        dst.triangles.add(triangle);
        // NOTE: triangle is not removed from src. This is implementation specific optimization.

        // Its up to the compiler to unroll everything.
        for (int i = 0; i < 3; i++) {
            if (triangle.vertexId[i] == oldID) {
                for (int n = 0; n < 3; n++) {
                    if (i != n) {
                        // This is implementation specific optimization to remove following line.
                        //removeEdge(triangle.vertex[i], new Edge(triangle.vertex[n]));

                        removeEdge(triangle.vertex[n], new Edge(triangle.vertex[i]));
                        addEdge(triangle.vertex[n], new Edge(dst));
                        addEdge(dst, new Edge(triangle.vertex[n]));
                    }
                }
                triangle.vertex[i] = dst;
                triangle.vertexId[i] = newID;
                return;
            }
        }
     //   assert (false);
    }
    
    private void updateVertexCollapseCost(Vertex vertex) {
        float collapseCost = UNINITIALIZED_COLLAPSE_COST;
        Vertex collapseTo = null;
        
        for (Edge edge : vertex.edges) {
            edge.collapseCost = computeEdgeCollapseCost(vertex, edge);
          //  assert (edge.collapseCost != UNINITIALIZED_COLLAPSE_COST);
            if (collapseCost > edge.collapseCost) {
                collapseCost = edge.collapseCost;
                collapseTo = edge.destination;
            }
        }
        if (collapseCost != vertex.collapseCost || vertex.collapseTo != collapseTo) {
//            assert (vertex.collapseTo != null);
//            assert (find(collapseCostSet, vertex));
            collapseCostSet.remove(vertex);
            if (collapseCost != UNINITIALIZED_COLLAPSE_COST) {
                vertex.collapseCost = collapseCost;
                vertex.collapseTo = collapseTo;
                collapseCostSet.add(vertex);
            }
        }
      //  assert (vertex.collapseCost != UNINITIALIZED_COLLAPSE_COST);
    }
    
    private boolean hasSrcID(int srcID, List<CollapsedEdge> cEdges) {
        // This will only return exact matches.
        for (CollapsedEdge collapsedEdge : cEdges) {
            if (collapsedEdge.srcID == srcID) {
                return true;
            }
        }
        
        return false; // Not found
    }
    
    private boolean collapse(Vertex src) {
        Vertex dest = src.collapseTo;
        if (src.edges.isEmpty()) {
            return false;
        }
//        assert (assertValidVertex(dest));
//        assert (assertValidVertex(src));
        
//        assert (src.collapseCost != NEVER_COLLAPSE_COST);
//        assert (src.collapseCost != UNINITIALIZED_COLLAPSE_COST);
//        assert (!src.edges.isEmpty());
//        assert (!src.triangles.isEmpty());
//        assert (src.edges.contains(new Edge(dest)));

        // It may have vertexIDs and triangles from different submeshes(different vertex buffers),
        // so we need to connect them correctly based on deleted triangle's edge.
        // mCollapsedEdgeIDs will be used, when looking up the connections for replacement.
        List<CollapsedEdge> tmpCollapsedEdges = new ArrayList<CollapsedEdge>();
        for (Iterator<Triangle> it = src.triangles.iterator(); it.hasNext();) {
            Triangle triangle = it.next();
            if (triangle.hasVertex(dest)) {
                // Remove a triangle
                // Tasks:
                // 1. Add it to the collapsed edges list.
                // 2. Reduce index count for the Lods, which will not have this triangle.
                // 3. Mark as removed, so it will not be added in upcoming Lod levels.
                // 4. Remove references/pointers to this triangle.

                // 1. task
                int srcID = triangle.getVertexIndex(src);
                if (!hasSrcID(srcID, tmpCollapsedEdges)) {
                    CollapsedEdge cEdge = new CollapsedEdge();
                    cEdge.srcID = srcID;
                    cEdge.dstID = triangle.getVertexIndex(dest);
                    tmpCollapsedEdges.add(cEdge);
                }

                // 2. task
                indexCount -= 3;

                // 3. task
                triangle.isRemoved = true;
                nbCollapsedTri++;

                // 4. task
                removeTriangleFromEdges(triangle, src);
                it.remove();
                
            }
        }
//        assert (!tmpCollapsedEdges.isEmpty());
//        assert (!dest.edges.contains(new Edge(src)));
        
        
        for (Iterator<Triangle> it = src.triangles.iterator(); it.hasNext();) {
            Triangle triangle = it.next();
            if (!triangle.hasVertex(dest)) {
                // Replace a triangle
                // Tasks:
                // 1. Determine the edge which we will move along. (we need to modify single vertex only)
                // 2. Move along the selected edge.

                // 1. task
                int srcID = triangle.getVertexIndex(src);
                int id = findDstID(srcID, tmpCollapsedEdges);
                if (id == Integer.MAX_VALUE) {
                    // Not found any edge to move along.
                    // Destroy the triangle.
                    //     if (!triangle.isRemoved) {
                    triangle.isRemoved = true;
                    indexCount -= 3;
                    removeTriangleFromEdges(triangle, src);
                    it.remove();
                    nbCollapsedTri++;
                    continue;
                }
                int dstID = tmpCollapsedEdges.get(id).dstID;

                // 2. task
                replaceVertexID(triangle, srcID, dstID, dest);
                
                
                if (bestQuality) {
                    triangle.computeNormal();
                }
                
            }
        }
        
        if (bestQuality) {
            for (Edge edge : src.edges) {
                updateVertexCollapseCost(edge.destination);
            }
            updateVertexCollapseCost(dest);
            for (Edge edge : dest.edges) {
                updateVertexCollapseCost(edge.destination);
            }
            
        } else {
            // TODO: Find out why is this needed. assertOutdatedCollapseCost() fails on some
            // rare situations without this. For example goblin.mesh fails.	
            //Treeset to have an ordered list with unique values
            SortedSet<Vertex> updatable = new TreeSet<Vertex>(collapseComparator);
            
            for (Edge edge : src.edges) {
                updatable.add(edge.destination);
                for (Edge edge1 : edge.destination.edges) {
                    updatable.add(edge1.destination);
                }
            }
            
            
            for (Vertex vertex : updatable) {
                updateVertexCollapseCost(vertex);
            }
            
        }
        return true;
    }
    
    private boolean assertValidMesh() {
        // Allows to find bugs in collapsing.
        for (Vertex vertex : collapseCostSet) {
            assertValidVertex(vertex);
        }
        return true;
        
    }
    
    private boolean assertValidVertex(Vertex v) {
        // Allows to find bugs in collapsing.
        //       System.out.println("Asserting " + v.index);
        for (Triangle t : v.triangles) {
            for (int i = 0; i < 3; i++) {
                //             System.out.println("check " + t.vertex[i].index);

                //assert (collapseCostSet.contains(t.vertex[i]));
                assert (find(collapseCostSet, t.vertex[i]));
                
                assert (t.vertex[i].edges.contains(new Edge(t.vertex[i].collapseTo)));
                for (int n = 0; n < 3; n++) {
                    if (i != n) {
                        
                        int id = t.vertex[i].edges.indexOf(new Edge(t.vertex[n]));
                        Edge ed = t.vertex[i].edges.get(id);
                        //assert (ed.collapseCost != UNINITIALIZED_COLLAPSE_COST);
                    } else {
                        assert (!t.vertex[i].edges.contains(new Edge(t.vertex[n])));
                    }
                }
            }
        }
        return true;
    }
    
    private boolean find(List<Vertex> set, Vertex v) {
        for (Vertex vertex : set) {
            if (v == vertex) {
                return true;
            }
        }
        return false;
    }
}
