package cz.fidentis.analyst.visitors.face;

import cz.fidentis.analyst.face.HumanFace;
import cz.fidentis.analyst.feature.FeaturePoint;
import cz.fidentis.analyst.feature.FeaturePointType;
import cz.fidentis.analyst.kdtree.KdTree;
import cz.fidentis.analyst.mesh.core.MeshFacet;
import cz.fidentis.analyst.visitors.mesh.HausdorffDistance;
import cz.fidentis.analyst.visitors.mesh.HausdorffDistance.Strategy;
import cz.fidentis.analyst.visitors.mesh.PrioritySphere;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.IntStream;
import javax.vecmath.Point3d;

/**
 * Decorator of the {@link cz.fidentis.analyst.visitors.mesh.HausdorffDistance} visitor.
 * This visitor computes priorities of Hausdorff distances with respect to feature points.
 * 
 * The visitor computes the following:
 * <ul>
 *   <li>Hausdorff distance, if necessary.</li>
 *   <li>Priorities of the faces' vertices with respect to the given types of feature
 *       points separately</li>
 *   <li>Priorities of the faces' vertices with respect to all the given types
 *       of feature points merged together</li>
 *   <li>Weighted average of Hausdorff distance of all vertices within the priority sphere
 *       of feature points of the given types for each of the faces' mesh facets</li>
 * </ul>
 * 
 * <p>
 * This visitor is thread-safe if the underlying HD visitor is thread-safe
 * (i.e., a single instance of the visitor can be used to inspect multiple human faces simultaneously).
 * </p>
 * 
 * @author Daniel Schramm
 * @author Radek Oslejsek
 */
public class HausdorffDistancePrioritized extends HumanFaceVisitor  {
    
    private final HausdorffDistance distanceVisitor;
    
    private final Map<FeaturePointType, Double> featurePointTypes;
    
    private final Map<HumanFace, Map<FeaturePointType, Map<MeshFacet, List<Double>>>> priorities = new HashMap<>();
    private final Map<HumanFace, Map<FeaturePointType, Map<MeshFacet, Double>>> featurePointWeights = new HashMap<>();
    private final Map<HumanFace, Map<MeshFacet, List<Double>>> mergedPriorities = new HashMap<>();
    
    /**
     * Constructor.
     * 
     * @param hdVisitor Underlying Hausdorff distance visitor. Must not be {@code null}
     * @param featurePoints Types of feature points according to which the Hausdorff distances will be prioritized
     * together with the radii of priority spheres around the feature points of the corresponding type.
     * Must not be {@code null}.
     * @throws IllegalArgumentException if some parameter is wrong or missing
     */
    public HausdorffDistancePrioritized(HausdorffDistance hdVisitor, Map<FeaturePointType, Double> featurePoints) {
        if (hdVisitor == null) {
            throw new IllegalArgumentException("hdVisitor");
        }
        this.distanceVisitor = hdVisitor;
        //distanceVisitor = new HausdorffDistance(mainFacets, strategy, relativeDistance, parallel);
        if (featurePoints == null) {
            throw new IllegalArgumentException("featurePoints");
        }
        for (final Double radius: featurePoints.values()) {
            if (radius == null || radius < 0) {
                throw new IllegalArgumentException("featurePoints");
            }
        }
        featurePointTypes = featurePoints;
    }
    
    /**
     * Returns types of feature points according to which the computation of Hausdorff
     * distance is prioritized together with the radii of priority spheres around
     * the feature points of the corresponding type.
     * 
     * @return Types of feature points according to which the computation of Hausdorff
     * distance is prioritized together with the radii of priority spheres around
     * the feature points of the corresponding type
     */
    public Map<FeaturePointType, Double> getFeaturePointTypes() {
        return Collections.unmodifiableMap(featurePointTypes);
    }
    
    /**
     * Returns Hausdorff distance of the visited faces' mesh facets to the source mesh facets. 
     * 
     * <p>
     * Keys in the map contain mesh facets that were measured with the 
     * source facets.<br>
     * For each facet of the visited human face, a list of distances to the source 
     * facets is stored. The order of distances corresponds to the order of vertices
     * in the measured facet, i.e., the i-th value is the distance of the i-th vertex
     * of the visited face's facet.
     * </p>
     * 
     * @return Hausdorff distance for all points of all the visited human faces' facets
     */
    public Map<MeshFacet, List<Double>> getDistances() {
        return distanceVisitor.getDistances();
    }
    
    /**
     * Returns the nearest points of the visited faces' mesh facets to the source mesh facets.
     * 
     * <p>
     * Keys in the map contain mesh facets that were measured with the
     * source facets.<br>
     * For each facet of the visited human face, a list of the nearest points to the source
     * facets is stored. The order of points corresponds to the order of vertices
     * in the measured facet, i.e., the i-th point is the nearest point of the i-th vertex
     * of the visited face's facet.
     * </p>
     *
     * @return The nearest points for all points of all the visited human faces' facets
     */
    public Map<MeshFacet, List<Point3d>> getNearestPoints() {
        return distanceVisitor.getNearestPoints();
    }

    /**
     * Returns priorities of vertices of the visited faces' mesh facets with respect
     * to the given types of feature points.
     * Priorities are calculated for each type of feature point separately.
     * 
     * <p>
     * Keys in the map contain human faces whose mesh facets were measured with the
     * source facets.<br>
     * For each human face, there is a map of examined types of feature points.<br>
     * Each feature point type then stores a map of priorities of vertices
     * of the face's mesh facets computed with respect to the face's feature point of
     * the corresponding type.
     * The order of priorities in the innermost map's value (list) corresponds to the order of vertices
     * in its corresponding key (facet), i.e., the i-th value in the list is the priority of
     * the i-th vertex of the facet.
     * </p>
     * 
     * @return Priorities of vertices with respect to the given types of feature points
     */
    public Map<HumanFace, Map<FeaturePointType, Map<MeshFacet, List<Double>>>> getPriorities() {
        return Collections.unmodifiableMap(priorities);
    }

    /**
     * For all visited faces' mesh facets, the method returns the weighted average of Hausdorff distance
     * of all vertices within the priority sphere of feature points of the given types
     * (i.e. the sum of weighted distances of the mesh facet's vertices to the source facets
     * divided by the sum of their priorities).
     * The weighted average is calculated for each type of feature point separately.
     * 
     * <p>
     * Keys in the map contain human faces whose mesh facets were measured with the
     * source facets.<br>
     * For each human face, there is a map of examined types of feature points.<br>
     * Each feature point type then stores a map of the face's mesh facets together with
     * the weighted average of distance of their vertices to the source facets.
     * The weighted average of distance is calculated with respect to the face's feature point
     * of the corresponding type.<br>
     * <i>If there is no vertex within the priority sphere of a feature point,
     * the value of the average weighted Hausdorff distance is {@link Double#NaN}</i>
     * </p>
     * 
     * @return Weighted average of Hausdorff distance of all vertices within the priority sphere
     *         of feature points of the given types
     */
    public Map<HumanFace, Map<FeaturePointType, Map<MeshFacet, Double>>> getFeaturePointWeights() {
        return Collections.unmodifiableMap(featurePointWeights);
    }

    /**
     * Returns priorities of vertices of the visited faces' mesh facets with respect
     * to all the given types of feature points merged together.
     * 
     * <p>
     * Keys in the map contain human faces whose mesh facets were measured with the
     * source facets.<br>
     * For each human face, there is a map of priorities of vertices of the visited
     * face's mesh facets. The priorities are computed with respect to all the face's
     * feature points of given types together.
     * The order of priorities in the inner map's value (list) corresponds to the order of vertices
     * in its corresponding key (facet), i.e., the i-th value in the list is the priority of
     * the i-th vertex of the facet.
     * </p>
     * 
     * @return Priorities of vertices with respect to all given types of feature
     *         points merged together
     */
    public Map<HumanFace, Map<MeshFacet, List<Double>>> getMergedPriorities() {
        return Collections.unmodifiableMap(mergedPriorities);
    }

    /**
     * Returns {@code true} if the distance was computed as relative 
     * (with possibly negative values). 
     * 
     * @return {@code true} if the distance is computed as relative, {@code false} otherwise
     */
    public boolean relativeDistance() {
        return distanceVisitor.relativeDistance();
    }
    
    /**
     * Returns the strategy of the computation of distance.
     * 
     * @return Strategy of the computation of distance
     */
    public Strategy getStrategy() {
        return distanceVisitor.getStrategy();
    }
    
    /**
     * Returns {@code true} if the distance computation is parallel.
     * 
     * @return {@code true} if the distance computation is parallel
     */
    public boolean inParallel() {
        return distanceVisitor.inParallel();
    }
    
    /**
     * Return the KD tree to which distances are computed.
     * 
     * @return KD tree to which distances are computed
     */
    public KdTree getMainKdTree() {
        return distanceVisitor.getMainKdTree();
    }
    
    @Override
    public void visitHumanFace(HumanFace humanFace) {
        // Compute the Hasudorff distance using the 'distanceVisitor', but only once
        if (!distanceVisitor.getDistances().keySet().containsAll(humanFace.getMeshModel().getFacets())) {
            humanFace.getMeshModel().compute(distanceVisitor, inParallel());
        }

        /*
         * If there are no feature points to be computed
         */
        if (featurePointTypes.isEmpty()) {
            final List<MeshFacet> faceFacets = humanFace.getMeshModel().getFacets();
            final Map<MeshFacet, List<Double>> faceMergedPriorities = new HashMap<>(faceFacets.size());
            for (final MeshFacet facet: faceFacets) {
                faceMergedPriorities.put(facet, Collections.nCopies(facet.getNumberOfVertices(), 0d));
            }

            synchronized (this) {
                priorities.put(humanFace, Map.of());
                featurePointWeights.put(humanFace, Map.of());
                mergedPriorities.put(humanFace, faceMergedPriorities);
            }

            return;
        }
        
        /*
         * Compute priorities of humanFace's vertices for each of the given feature point types
         */
        final Map<MeshFacet, List<Double>> hausdorffDistances = distanceVisitor.getDistances();
        for (final Map.Entry<FeaturePointType, Double> fpType: featurePointTypes.entrySet()) {
            final FeaturePointType featurePointType = fpType.getKey();
            final double featurePointRadius = fpType.getValue();
            
            final FeaturePoint featurePoint = getFeaturePointByType(humanFace, featurePointType);
            if (featurePoint == null) {
                continue;
            }
            
            final PrioritySphere priorityVisitor = new PrioritySphere(featurePoint.getPosition(), featurePointRadius);
            humanFace.getMeshModel().compute(priorityVisitor, inParallel());

            synchronized (this) {
                priorities.computeIfAbsent(humanFace, face -> new HashMap<>())
                        .computeIfAbsent(featurePointType, fPointType -> new HashMap<>())
                        .putAll(priorityVisitor.getPriorities());
            }
            
            for (final Map.Entry<MeshFacet, List<Double>> entry: priorityVisitor.getPriorities().entrySet()) {
                final MeshFacet facet = entry.getKey();
                final List<Double> facetPriorities = entry.getValue();

                synchronized (this) {
                    /*
                     * Calculate the weighted average of Hausdorff distance
                     * of all vertices within the feature point's priority sphere.
                     */
                    final List<Double> facetDistances = hausdorffDistances.get(facet);
                    featurePointWeights.computeIfAbsent(humanFace, face -> new HashMap<>())
                            .computeIfAbsent(featurePointType, fPointType -> new HashMap<>())
                            .put(facet, IntStream.range(0, facetDistances.size())
                                    .filter(i -> facetPriorities.get(i) > 0) // Filter out vertices that are outside of the priority sphere
                                    .boxed()
                                    .collect(WeightedAverageCollector.toWeightedAverage(
                                            facetDistances::get,
                                            facetPriorities::get
                                    ))
                            );
                    
                    /* 
                     * Merge priorities computed for the current feature point type
                     * with the priorities of already computed feature point types.
                     */
                    final List<Double> storedFacetPriorities = mergedPriorities
                            .computeIfAbsent(humanFace, face -> new HashMap<>())
                            .get(facet);
                    if (storedFacetPriorities == null) { // No priorities stored for this facet yet -> there is nothing to be merged
                        mergedPriorities.get(humanFace).put(facet, new ArrayList<>(facetPriorities));
                        continue;
                    }

                    // Merge priorities -> take the greater value
                    for (int i = 0; i < storedFacetPriorities.size(); i++) {
                        storedFacetPriorities.set(i, Double.max(storedFacetPriorities.get(i), facetPriorities.get(i)));
                    }
                }
            }
        }
    }
    
    /**
     * Finds and returns a feature point of the desired type in the given human face.
     * 
     * @param face Face containing the desired feature point
     * @param type Type of the feature point to be obtained
     * @return Feature point of the desired type
     */
    protected FeaturePoint getFeaturePointByType(HumanFace face, FeaturePointType type) {
        for (final FeaturePoint featurePoint: face.getFeaturePoints()) {
            if (type.getType() == featurePoint.getFeaturePointType().getType()) {
                return featurePoint;
            }
        }
        
        return null;
    }
}
