package cz.fidentis.analyst.symmetry;

import cz.fidentis.analyst.mesh.MeshVisitor;
import cz.fidentis.analyst.mesh.core.MeshFacet;
import cz.fidentis.analyst.mesh.core.MeshPoint;
import cz.fidentis.analyst.visitors.mesh.Curvature;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.PriorityQueue;
import javax.vecmath.Vector3d;

/**
 * Visitor selecting X most significant vertices of inspected mesh facets. 
 * The most significant vertices are those having the highest curvature.
 * <p>
 * This visitor <b>is not thread-safe</b> for the efficiency reasons, i.e., 
 * a single instance of the visitor cannot be used to inspect multiple meshes 
 * simultaneously (sequential inspection is okay).
 * </p>
 * 
 * @author Radek Oslejsek
 * @author Natalia Bebjakova
 */
public class SignificantPoints extends MeshVisitor {
    
    private final int maxPoints;
    
    private final Curvature curvatureVisitor;
    
    private final CurvatureAlg curvatureAlg;
    
    private List<VertCurvature> significantPoints;
    
    private List<List<Vector3d>> normCosVecCache;
    
    private List<List<Vector3d>> avgPosCache;
    
    private List<List<Double>> curRatioCache;
    
    /**
     * Constructor.
     * 
     * @param cAlg Curvature strategy to be used for the selection of significant points.
     * @param max Maximal number of significant points
     * @throws IllegalArgumentException if the {@code curbatureVisitor} is missing
     */
    public SignificantPoints(CurvatureAlg cAlg, int max) {
        this(new Curvature(), cAlg, max);
    }
    
    /**
     * Constructor.
     * 
     * @param curvatureVisitor Curvature. If the object has
     * pre-filled curvatures of meshes, then they are used also used for the computation
     * of significant points. In this way, it is possible to reuse already computed
     * curvatures and skip calling the {@link #visitMeshFacet} inspection method.
     * @param cAlg Curvature strategy to be used for the selection of significant points.
     * @param max Maximal number of significant points
     * @throws IllegalArgumentException if the {@code curbatureVisitor} is missing
     */
    public SignificantPoints(Curvature curvatureVisitor, CurvatureAlg cAlg, int max) {
        if (curvatureVisitor == null) {
            throw new IllegalArgumentException("curvatureVisitor");
        }
        this.maxPoints = max;
        this.curvatureVisitor = curvatureVisitor;
        this.curvatureAlg = cAlg;
    }
    
    @Override
    public boolean isThreadSafe() {
        return false;
    }
    
    @Override
    public void visitMeshFacet(MeshFacet facet) {
        significantPoints = null; // clear previous results
        curvatureVisitor.visitMeshFacet(facet); // compute curvature for new inspected facet
    }
    
    /**
     * Returns cached normal vector for cos.
     * @param i index of the i-th significant curvature point
     * @param j index of the j-th significant curvature point
     * @return cached value or null
     */
    public Vector3d getCachedNormCosVec(int i, int j) {
        checkAndComputeSignificantPoints();
        if (normCosVecCache == null || i >= normCosVecCache.size() || i >= normCosVecCache.size() || i < 0 || j < 0) {
            return null;
        }
        return normCosVecCache.get(i).get(j);
    }
    
    /**
     * Returns cached vector for the computation average normal.
     * @param i index of the i-th significant curvature point
     * @param j index of the j-th significant curvature point
     * @return cached value or null
     */
    public Vector3d getCachedAvgPos(int i, int j) {
        checkAndComputeSignificantPoints();
        if (avgPosCache == null || i >= avgPosCache.size() || i >= avgPosCache.size() || i < 0 || j < 0) {
            return null;
        }
        return avgPosCache.get(i).get(j);
    }
    
    /**
     * Returns cached curvature ratio.
     * @param i index of the i-th significant curvature point
     * @param j index of the j-th significant curvature point
     * @return cached value or {@code Double.NaN}
     */
    public double getCachedCurRatio(int i, int j) {
        checkAndComputeSignificantPoints();
        if (curRatioCache == null || i >= curRatioCache.size() || i >= curRatioCache.size() || i < 0 || j < 0) {
            return Double.NaN;
        }
        return curRatioCache.get(i).get(j);
    }
    
    /**
     * Returns index of the i-th most significant mesh vertex.
     * 
     * @param i The "i" (index of the ordering)
     * @return index of the i-th most significant mesh vertex.
     * @throws IllegalArgumentException if the {@code i} parameter is out of rage
     */
    public int getVertexIndex(int i) {
        checkAndComputeSignificantPoints();
        if (i < 0 || i >= significantPoints.size()) {
            throw new IllegalArgumentException("i");
        }
        return significantPoints.get(i).vertIndex;
    }
    
    /**
     * Returns curvature of the i-th most significant mesh vertex.
     * 
     * @param i The "i" (index of the ordering)
     * @return curvature of the i-th most significant mesh vertex.
     * @throws IllegalArgumentException if the {@code i} parameter is out of rage
     */
    public double getCurvature(int i) {
        checkAndComputeSignificantPoints();
        if (i < 0 || i >= significantPoints.size()) {
            throw new IllegalArgumentException("i");
        }
        return significantPoints.get(i).curvature;
    }
    
    /**
     * Returns mesh facet of the i-th most significant mesh vertex.
     * 
     * @param i The "i" (index of the ordering)
     * @return mesh facet of the i-th most significant mesh vertex.
     * @throws IllegalArgumentException if the {@code i} parameter is out of rage
     */
    public MeshFacet getMeshFacet(int i) {
        checkAndComputeSignificantPoints();
        if (i < 0 || i >= significantPoints.size()) {
            throw new IllegalArgumentException("i");
        }
        return significantPoints.get(i).facet;
    }
    
    /**
     * Returns number of the most significant points.
     * @return number of the most significant points.
     */
    public int size() {
        checkAndComputeSignificantPoints();
        return significantPoints.size();
    }
    
    protected void checkAndComputeSignificantPoints() {
        if (significantPoints != null) {
            return; // already computed
        }
        
        // fill the priority queue
        final PriorityQueue<VertCurvature> priorityQueue = new PriorityQueue<>();
        
        Map<MeshFacet, List<Double>> curvMap = null;
        switch (curvatureAlg) {
            case MEAN: 
                curvMap = curvatureVisitor.getMeanCurvatures();
                break;
            case GAUSSIAN: 
                curvMap = curvatureVisitor.getGaussianCurvatures();
                break;
            case MAX: 
                curvMap = curvatureVisitor.getMaxPrincipalCurvatures();
                break;
            case MIN: 
                curvMap = curvatureVisitor.getMinPrincipalCurvatures();
                break;
            default:
                throw new IllegalArgumentException("curvatureAlg");
        }

        for (MeshFacet facet: curvMap.keySet()) {
            List<Double> curvs = curvMap.get(facet);
            for (int i = 0; i < curvs.size(); i++) { 
                // store helper objects, replace NaN with 0.0:
                double c = curvs.get(i);
                priorityQueue.add(new VertCurvature(facet, i, (Double.isNaN(c) ? 0.0 : c)));
            }  
        }
        
        // select top significant points
        significantPoints = new ArrayList<>(maxPoints);
        for (int i = 0; i < maxPoints; i++) {
            VertCurvature vc = priorityQueue.poll();
            if (vc == null) {
                break; // no more points available
            } else {
                significantPoints.add(vc);
            }
        }
        
        // finally, precompute and cache data of the significant points
        cacheData();
    }
    
    /**
     * Pre-computes and caches some values related to the computation of
     * asymmetric plane from curvature.
     */
    protected void cacheData() {
        normCosVecCache = new ArrayList<>(significantPoints.size());
        avgPosCache = new ArrayList<>(significantPoints.size());
        curRatioCache = new ArrayList<>(significantPoints.size());
        
        for (int i = 0; i < significantPoints.size(); i++) {
            List<Vector3d> cosArray = new ArrayList<>(significantPoints.size());
            normCosVecCache.add(cosArray);
            
            List<Vector3d> posArray = new ArrayList<>(significantPoints.size());
            avgPosCache.add(posArray);
            
            List<Double> curArray = new ArrayList<>(significantPoints.size());
            curRatioCache.add(curArray);
            
            for (int j = 0; j < significantPoints.size(); j++) {
                VertCurvature vcI = significantPoints.get(i);
                VertCurvature vcJ = significantPoints.get(j);
                MeshPoint meshPointI = vcI.facet.getVertex(vcI.vertIndex);
                MeshPoint meshPointJ = vcJ.facet.getVertex(vcJ.vertIndex);
                
                Vector3d ni = new Vector3d(meshPointI.getNormal());
                Vector3d nj = new Vector3d(meshPointJ.getNormal());
                ni.normalize();
                nj.normalize();
                ni.sub(nj);
                ni.normalize();
                cosArray.add(ni);
                
                Vector3d avrg = new Vector3d(meshPointI.getPosition());
                Vector3d aux = new Vector3d(meshPointJ.getPosition());
                avrg.add(aux);
                avrg.scale(0.5);
                posArray.add(avrg);
                curArray.add(vcI.curvature / vcJ.curvature);
            }
        }
    }
        
    /**
     * Helper class for sorting points with respect to their curvature.
     * @author Radek Oslejsek
     */
    private class VertCurvature implements Comparable<VertCurvature> {
        
        private final int vertIndex;
        private final double curvature;
        private final MeshFacet facet;
        
        VertCurvature(MeshFacet facet, int vertIndex, double curvature) {
            this.vertIndex = vertIndex;
            this.curvature = curvature;
            this.facet = facet;
        }

        /**
         * Compares this object with the specified object for order. 
         * Curvature is taken into consideration as the primary value (descendant ordering).
         * If the curvature equals, then the objects are sorted randomly (1 is always returned).
         * This approach preserves all points in sorted collections.
         * 
         * @param arg the object to be compared.
         * @return a negative integer, zero, or a positive integer as this object 
         * is less than, equal to, or greater than the specified object.
         */
        @Override
        public int compareTo(VertCurvature arg) {
            int comp = Double.compare(arg.curvature, this.curvature);
            return (comp == 0) ? 1 : comp;
        }
        
        public String toString() {
            return ""+curvature;
        }
    }
    
}
