package cz.fidentis.analyst.visitors.mesh;

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.DistanceWithNearestPoints;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.vecmath.Point3d;
import javax.vecmath.Vector3d;

/**
 * This visitor finds the distance between the given 3D point and a mesh facet.
 * <p>
 * The minimal distance is computed between the 3D point and triangles of the mesh facets,
 * i.e., the closest point at the mesh surface is found. 
 * This method is only a bit slower than the computation of the distance to mesh vertices, 
 * but more precise.
 * </p>
 * <p>
 * <b>Use with caution!</b> This visitor find the closest mesh vertex first and then 
 * explores triangles around the vertex to find the closest triangle. It is more
 * optimal than computing the distance to all triangles, but it works only for rather 
 * flat surfaces, e.g., parts of human face that we are interested in. 
 * For general meshes, the results can be noisy. 
 * </p>
 * <p>
 * The visitor returns the minimal distance, corresponding mesh triangles, and their closest points.
 * </p>
 * <p>
 * This visitor is thread-safe.
 * </p>
 *
 * @author Radek Oslejsek
 */
public class MeshApproxDistanceToTriangles extends MeshVisitor implements DistanceWithNearestPoints {

    private double distance = Double.POSITIVE_INFINITY;
    private final Point3d point3d;
    private Map<MeshFacet, List<Point3d>> nearestPoints = new HashMap<>();
    
    /**
     * @param point A 3D point from which distance is computed. Must not be {@code null}
     * @throws IllegalArgumentException if some parameter is wrong
     */
    public MeshApproxDistanceToTriangles(Point3d point) {
        if (point == null) {
            throw new IllegalArgumentException("point");
        }
        this.point3d = point;
    }
    
    @Override
    public Map<MeshFacet, List<Point3d>> getNearestPoints() {
        return Collections.unmodifiableMap(nearestPoints);
    }

    @Override
    public double getDistance() {
        return distance;
    }

    @Override
    public void visitMeshFacet(MeshFacet facet) {
        // find the closest vertex:
        double minDist = Double.POSITIVE_INFINITY;
        List<Integer> closestVerts = new ArrayList<>();
        List<MeshPoint> vertices = facet.getVertices();
        for (int i = 0; i < vertices.size(); i++) {
            Point3d pointOnSurface = vertices.get(i).getPosition();
            Vector3d aux = new Vector3d(pointOnSurface);
            aux.sub(point3d);
            double dist = aux.length();
            
            if (dist > minDist) {
                continue;
            }
            if (dist < minDist) {
                minDist = dist;
                closestVerts.clear();   
            }
            closestVerts.add(i);
        }
        
        
        // explore surrounding triangles:
        for (int i = 0; i < closestVerts.size(); i++) { // for all closest vertices
            final Point3d projection = facet.getClosestAdjacentPoint(point3d, closestVerts.get(i));
            final Vector3d aux = new Vector3d(projection);
            aux.sub(point3d);
            final double dist = aux.length();
            
            synchronized (this) {
                if (dist > distance) {
                    continue;
                }
                if (dist < distance) {
                    distance = dist;
                    nearestPoints.clear();                    
                }
                nearestPoints.computeIfAbsent(facet, meshFacet -> new ArrayList<>())
                        .add(projection);
            }
        }
    }
}
