package cz.fidentis.analyst.visitors.kdtree;

import cz.fidentis.analyst.kdtree.KdNode;
import cz.fidentis.analyst.kdtree.KdTree;
import cz.fidentis.analyst.kdtree.KdTreeVisitor;
import cz.fidentis.analyst.mesh.core.MeshFacet;
import cz.fidentis.analyst.visitors.DistanceWithNearestPoints;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import javax.vecmath.Point3d;

/**
 * This visitor finds the minimal distance between the given 3D point and and points 
 * (e.g., mesh vertices) stored in k-d trees. 
 * All closest mesh facets and all their closest vertices are returned in addition to the distance.
 * <p>
 * This visitor is thread-safe, i.e., a single instance of the visitor can be used 
 * to inspect multiple k-d trees simultaneously.
 * </p>
 * 
 * @author Daniel Schramm
 * @author Radek Oslejsek
 */
public class KdTreeDistanceToVertices extends KdTreeVisitor implements DistanceWithNearestPoints {
    
    private double distance = Double.POSITIVE_INFINITY;
    private final Point3d point3d;
    private Map<MeshFacet, List<Integer>> nearestVertices = new HashMap<>();
    private final boolean checkOverlay;
    
    /**
     * Constructor.
     * 
     * @param point A 3D point from which distance is computed. Must not be {@code null}
     * @param checkOverlay If {@code true} and and closest point to the given 3D reference point 
     *        lies on the boundary of the mesh stored in the k-d tree, 
     *        then the reference point is considered "outside" of the mesh
     *        and the distance is set to infinity.
     * @throws IllegalArgumentException if some parameter is wrong
     */
    public KdTreeDistanceToVertices(Point3d point, boolean checkOverlay) {
        if (point == null) {
            throw new IllegalArgumentException("point");
        }
        this.point3d = point;
        this.checkOverlay = checkOverlay;
    }
    
    @Override
    public Map<MeshFacet, List<Point3d>> getNearestPoints() {
        Map<MeshFacet, List<Point3d>> ret = new HashMap<>();
        nearestVertices.keySet().forEach(f -> {
            nearestVertices.get(f).forEach(i -> {
                ret.computeIfAbsent(f, n -> new ArrayList<>())
                        .add(f.getVertex(i).getPosition());
            });
        });
        return ret;
    }
        
    @Override
    public double getDistance() {
        if (!checkOverlay || distance == Double.POSITIVE_INFINITY) {
            return distance;
        }
        
        for (MeshFacet f: nearestVertices.keySet()) {
            for (Integer i: nearestVertices.get(f)) {
                if (f.getOneRingNeighborhood(i).isBoundary()) {
                    return Double.POSITIVE_INFINITY;
                }
            }
        }
        
        return distance;
    }

    @Override
    public void visitKdTree(KdTree kdTree) {
        final KdTreeClosestNode visitor = new KdTreeClosestNode(point3d);
        kdTree.accept(visitor);
        final Set<KdNode> closestNodes = visitor.getClosestNodes();
        final double dist = visitor.getDistance();
        
        synchronized (this) {
            if (dist < distance) {
                distance = dist;
                nearestVertices.clear();                    
            }
        }
        
        for (final KdNode node: closestNodes) { // For all nodes
            synchronized (this) {
                if (dist > distance) {
                    return;
                }
                for (Entry<MeshFacet, Integer> entry: node.getFacets().entrySet()) {
                    MeshFacet facet = entry.getKey();
                    nearestVertices.computeIfAbsent(facet, n -> new ArrayList<>())
                            .add(entry.getValue());
                }
            }
        }
    }
}
