package cz.fidentis.analyst.distance;

import com.jogamp.opengl.GL2;
import cz.fidentis.analyst.canvas.Canvas;
import cz.fidentis.analyst.core.LoadedActionEvent;
import cz.fidentis.analyst.core.ControlPanelAction;
import cz.fidentis.analyst.core.ControlPanelBuilder;
import cz.fidentis.analyst.core.OutputWindow;
import cz.fidentis.analyst.feature.FeaturePoint;
import cz.fidentis.analyst.feature.FeaturePointType;
import cz.fidentis.analyst.mesh.core.MeshFacet;
import cz.fidentis.analyst.scene.DrawableFeaturePoints;
import cz.fidentis.analyst.visitors.face.HausdorffDistancePrioritized;
import cz.fidentis.analyst.visitors.mesh.HausdorffDistance.Strategy;
import java.awt.Color;
import java.awt.event.ActionEvent;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
import javax.swing.JComboBox;
import javax.swing.JTabbedPane;
import javax.swing.JTextField;
import javax.swing.JToggleButton;
import javax.vecmath.Point3d;

/**
 * Action listener for computation of the Hausdorff distance.
 * 
 * @author Radek Oslejsek
 * @author Daniel Schramm
 */
public class DistanceAction extends ControlPanelAction {
    
    /*
     * Attributes handling the state
     */
    private HausdorffDistancePrioritized visitor = null;
    private final Map<FeaturePointType, Double> featurePointTypes;
    private String strategy = DistancePanel.STRATEGY_POINT_TO_POINT;
    private boolean relativeDist = false;
    private String heatmapDisplayed = DistancePanel.HEATMAP_HAUSDORFF_DISTANCE;
    private boolean weightedFPsShow = true;
    
    private FeaturePointType hoveredFeaturePoint = null;
    
    private final DistancePanel controlPanel;
    private final DrawableFeaturePoints weightedFeaturePoints;
    
    private static final Color WEIGHTED_FEATURE_POINT_DEFAULT_COLOR = Color.WHITE;
    private static final Color FEATURE_POINT_HIGHLIGHT_COLOR = Color.MAGENTA;
    private static final Color FEATURE_POINT_HOVER_COLOR = Color.CYAN;
    
    /**
     * Constructor.
     * 
     * @param canvas OpenGL canvas
     * @param topControlPanel Top component for placing control panels
     */
    public DistanceAction(Canvas canvas, JTabbedPane topControlPanel) {
        super(canvas, topControlPanel);
        
        // Calculate weighted Hausdorff distance for all feature points of the secondary face
        calculateHausdorffDistance(
                getSecondaryFeaturePoints().getFeaturePoints()
                        .stream()
                        .collect(Collectors.toMap(
                                FeaturePoint::getFeaturePointType,
                                featurePoint -> DrawableFeaturePoints.DEFAULT_SIZE))
        );
        
        this.featurePointTypes = visitor.getFeaturePointWeights()
                .get(getSecondaryDrawableFace().getHumanFace()) // Get FP weights for the secondary face
                .entrySet()
                .stream()
                .map(fpWeights -> Map.entry(
                        fpWeights.getKey(),  // For each FP type at the secondary face...
                        fpWeights.getValue() // ... compute average FP weight over all its facets
                                .values()
                                .stream()
                                .mapToDouble(Double::doubleValue)
                                .average()
                                .orElse(Double.NaN)))
                .filter(fpWeight -> !Double.isNaN(fpWeight.getValue())) // Filter out feature points with Double.NaN weight
                .collect(Collectors.toMap(
                        Map.Entry::getKey,
                        fpType -> DrawableFeaturePoints.DEFAULT_SIZE));
        this.controlPanel = new DistancePanel(this, getSecondaryFeaturePoints().getFeaturePoints(), featurePointTypes.keySet());
        this.visitor = null;
        
        // Add weighted feature points to the scene
        this.weightedFeaturePoints = new DrawableFeaturePoints(getSecondaryFeaturePoints().getFeaturePoints());
        weightedFeaturePoints.setRenderMode(GL2.GL_LINE);
        weightedFeaturePoints.setColor(WEIGHTED_FEATURE_POINT_DEFAULT_COLOR);
        getCanvas().getScene().addOtherDrawable(weightedFeaturePoints);
        
        // Place control panel to the topControlPanel
        topControlPanel.addTab(controlPanel.getName(), controlPanel.getIcon(), controlPanel);
        topControlPanel.addChangeListener(e -> {
            // If the distance panel is focused...
            if (((JTabbedPane) e.getSource()).getSelectedComponent() instanceof DistancePanel) {
                // ... display heatmap and feature points relevant to the Hausdorff distance
                getCanvas().getScene().setDefaultColors();
                weightedFeaturePoints.resetAllColorsToDefault();
                if (weightedFPsShow) {
                    weightedFeaturePoints.show();
                }
                
                boolean secondaryFaceMoved = false;
                final List<FeaturePoint> secondaryFPs = getSecondaryFeaturePoints().getFeaturePoints();
                final List<FeaturePoint> weightedFPs = weightedFeaturePoints.getFeaturePoints();
                for (int i = 0; i < secondaryFPs.size(); i++) {
                    final FeaturePoint faceFP = secondaryFPs.get(i);
                    final FeaturePoint weightedFP = weightedFPs.get(i);
                    
                    // Highlight feature point if selected
                    if (featurePointTypes.containsKey(faceFP.getFeaturePointType())) {
                        colorSecondaryFaceFeaturePoint(i, FEATURE_POINT_HIGHLIGHT_COLOR);
                    }
                    if (!faceFP.getPosition().equals(weightedFP.getPosition())) {
                        secondaryFaceMoved = true;
                    }
                }
                
                // If the position of secondary face has been changed
                if (secondaryFaceMoved) {
                    visitor = null; // recompute Hausdorff distance
                    
                    // Relocate weighted feature points
                    for (int i = 0; i < secondaryFPs.size(); i++) {
                        final Point3d faceFpPos = secondaryFPs.get(i).getPosition();
                        final Point3d weightedFpPos = weightedFPs.get(i).getPosition();
                        
                        weightedFpPos.x = faceFpPos.x;
                        weightedFpPos.y = faceFpPos.y;
                        weightedFpPos.z = faceFpPos.z;
                    }
                }
                
                updateHausdorffDistanceInformation();
                getSecondaryDrawableFace().setRenderHeatmap(isHeatmapDisplayed());
            } else {
                weightedFeaturePoints.hide();
                getSecondaryDrawableFace().clearHeatMapSaturation();
            }
        });
        topControlPanel.setSelectedComponent(controlPanel); // Focus Hausdorff distance panel
    }

    @Override
    public void actionPerformed(ActionEvent ae) {
        final String action = ae.getActionCommand();
        
        switch (action) {
            case DistancePanel.ACTION_COMMAND_SET_DISPLAYED_HEATMAP:
                heatmapDisplayed = (String) ((JComboBox) ae.getSource()).getSelectedItem();
                updateHausdorffDistanceInformation();
                getSecondaryDrawableFace().setRenderHeatmap(isHeatmapDisplayed());
                break;
            case DistancePanel.ACTION_COMMAND_SHOW_HIDE_WEIGHTED_FPOINTS:
                weightedFPsShow = ((JToggleButton) ae.getSource()).isSelected();
                if (weightedFPsShow) {
                    weightedFeaturePoints.show();
                } else {
                    weightedFeaturePoints.hide();
                }
                break;
            case DistancePanel.ACTION_COMMAND_SET_DISTANCE_STRATEGY:
                strategy = (String) ((JComboBox) ae.getSource()).getSelectedItem();
                recompute();
                break;
            case DistancePanel.ACTION_COMMAND_RELATIVE_ABSOLUTE_DIST:
                this.relativeDist = ((JToggleButton) ae.getSource()).isSelected();
                recompute();
                break;
            case DistancePanel.ACTION_COMMAND_FEATURE_POINT_HIGHLIGHT:
                highlightFeaturePoint((LoadedActionEvent) ae);
                recompute();
                break;
            case DistancePanel.ACTION_COMMAND_FEATURE_POINT_SELECT_ALL:
                highlightAllFeaturePoints(true);
                recompute();
                break;
            case DistancePanel.ACTION_COMMAND_FEATURE_POINT_SELECT_NONE:
                highlightAllFeaturePoints(false);
                recompute();
                break;
            case DistancePanel.ACTION_COMMAND_FEATURE_POINT_HOVER_IN:
                hoverFeaturePoint((LoadedActionEvent) ae, true);
                break;
            case DistancePanel.ACTION_COMMAND_FEATURE_POINT_HOVER_OUT:
                hoverFeaturePoint((LoadedActionEvent) ae, false);
                break;
            case DistancePanel.ACTION_COMMAND_FEATURE_POINT_RESIZE:
                resizeFeaturePoint((LoadedActionEvent) ae);
                break;
            case DistancePanel.ACTION_COMMAND_DISTANCE_RECOMPUTE:
                recompute();
                break;
            default:
                // to nothing
        }
        renderScene();
    }
    
    /**
     * Recalculates the Hausdorff distance and updates all GUI elements
     * of {@link DistancePanel}.
     */
    private void recompute() {
        this.visitor = null;
        updateHausdorffDistanceInformation();
    }
    
    /**
     * (Re)calculates the Hausdorff distance and updates the heat map of the secondary face
     * as well as values of all appropriate GUI elements of {@link DistancePanel}.
     */
    private void updateHausdorffDistanceInformation() {
        if (visitor == null) {
            calculateHausdorffDistance(featurePointTypes);
            
            // Update GUI elements that display the calculated Hausdorff distance metrics
            setFeaturePointWeigths();
            setHausdorffDistanceStatistics();
        }
        
        switch (heatmapDisplayed) {
            case DistancePanel.HEATMAP_HAUSDORFF_DISTANCE:
                getSecondaryDrawableFace().clearHeatMapSaturation();
                break;
            case DistancePanel.HEATMAP_WEIGHTED_HAUSDORFF_DISTANCE:
                getSecondaryDrawableFace().setHeatMapSaturation(
                        visitor.getMergedPriorities()
                                .get(getSecondaryDrawableFace().getHumanFace())
                );
                break;
            case DistancePanel.HEATMAP_HIDE:
                return;
            default:
                throw new UnsupportedOperationException(heatmapDisplayed);
        }
        getSecondaryDrawableFace().setHeatMap(visitor.getDistances());
    }
    
    /**
     * (Re)calculates the Hausdorff distance.
     * 
     * @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}.
     */
    private void calculateHausdorffDistance(Map<FeaturePointType, Double> featurePoints) {
        final Strategy useStrategy;
        switch (strategy) {
            case DistancePanel.STRATEGY_POINT_TO_POINT:
                useStrategy = Strategy.POINT_TO_POINT;
                break;
            case DistancePanel.STRATEGY_POINT_TO_TRIANGLE:
                useStrategy = Strategy.POINT_TO_TRIANGLE_APPROXIMATE;
                break;
            default:
                throw new UnsupportedOperationException(strategy);
        }
        
        OutputWindow out = OutputWindow.measureTime();
        this.visitor = new HausdorffDistancePrioritized(getPrimaryDrawableFace().getModel(),
                featurePoints,
                useStrategy,
                relativeDist,
                true);
        getSecondaryDrawableFace().getHumanFace().accept(visitor);
        out.printDuration("Computation of Hausdorff distance for models with " 
                + getPrimaryDrawableFace().getHumanFace().getMeshModel().getNumVertices()
                + "/"
                + getSecondaryDrawableFace().getHumanFace().getMeshModel().getNumVertices()
                + " vertices"
        );
    }
    
    /**
     * Updates the GUI elements of {@link DistancePanel} that display
     * the weights of feature points used to calculate the weighted Hausdorff distance.
     */
    private void setFeaturePointWeigths() {
        controlPanel.updateFeaturePointWeights(
                visitor.getFeaturePointWeights()
                        .get(getSecondaryDrawableFace().getHumanFace()) // Get FP weights for the secondary face
                        .entrySet()
                        .stream()
                        .collect(
                                Collectors.toMap(
                                        Map.Entry::getKey,            // For each FP type at the secondary face...
                                        weights -> weights.getValue() // ... compute average FP weight over all its facets
                                                .values()
                                                .stream()
                                                .mapToDouble(Double::doubleValue)
                                                .average()
                                                .orElse(Double.NaN))));
    }

    /**
     * Updates the GUI elements of {@link DistancePanel} elements that display
     * statistical data about the calculated Hausdorff distance.
     */
    private void setHausdorffDistanceStatistics() {
        controlPanel.updateHausdorffDistanceStats(
                visitor.getDistances()
                        .values()
                        .stream()
                        .flatMap(List::stream)
                        .mapToDouble(Double::doubleValue)
                        .summaryStatistics(),
                getWeightedDistance()
                        .values()
                        .stream()
                        .flatMap(List::stream)
                        .mapToDouble(Double::doubleValue)
                        .summaryStatistics()
        );
    }
    
    /**
     * Calculates weighted Hausdorff distance of the face.
     * 
     * @return weighted Hausdorff distance
     */
    private Map<MeshFacet, List<Double>> getWeightedDistance() {
        final Map<MeshFacet, List<Double>> weightedDistances = new HashMap<>(visitor.getDistances());
        final Map<MeshFacet, List<Double>> mergedPriorities = visitor.getMergedPriorities()
                .get(getSecondaryDrawableFace().getHumanFace());

        // Merge the map of distances with the map of priorities
        for (final Map.Entry<MeshFacet, List<Double>> facetPriorities: mergedPriorities.entrySet()) {
            weightedDistances.merge(
                    facetPriorities.getKey(),
                    facetPriorities.getValue(),
                    (distancesList, prioritiesList) ->
                            IntStream.range(0, distancesList.size())
                                    .mapToDouble(i -> distancesList.get(i) * prioritiesList.get(i))
                                    .boxed()
                                    .collect(Collectors.toList()));
        }

        return weightedDistances;
    }
    
    /**
     * Returns {@code true} if the heatmap is displayed and {@code false} otherwise.
     * 
     * @return {@code true} if the heatmap is displayed, {@code false} otherwise
     */
    private boolean isHeatmapDisplayed() {
        return !DistancePanel.HEATMAP_HIDE.equals(heatmapDisplayed);
    }
    
    /**
     * Changes the color of the secondary face's feature point at the given index
     * and of its weighted representation when the feature point is (de)selected
     * for the computation of the weighted Hausdorff distance.
     * The index is received as the data payload of {@code actionEvent}.
     * 
     * @param actionEvent Action event with the index of the feature point as its payload data
     */
    private void highlightFeaturePoint(LoadedActionEvent actionEvent) {
        final int index = (int) actionEvent.getData();
        final FeaturePointType fpType = getTypeOfFeaturePoint(index);
        
        if (((JToggleButton) actionEvent.getSource()).isSelected()) {
            colorSecondaryFaceFeaturePoint(index, FEATURE_POINT_HIGHLIGHT_COLOR);
            featurePointTypes.put(fpType, weightedFeaturePoints.getSize(index));
        } else {
            resetSecondaryFaceFeaturePointColor(index);
            featurePointTypes.remove(fpType);
        }
    }
    
    /**
     * Changes the color of all secondary face's feature points and of their
     * weighted representations.
     * All feature points are also (de)selected for the computation
     * of the weighted Hausdorff distance.
     * 
     * @param highlighted {@code true} if the feature points are to be highlighted,
     *                    {@code false} otherwise
     */
    private void highlightAllFeaturePoints(boolean highlighted) {
        if (highlighted) {
            final List<FeaturePoint> featurePoints = getSecondaryFeaturePoints().getFeaturePoints();
            for (int i = 0; i < featurePoints.size(); i++) {
                colorSecondaryFaceFeaturePoint(i, FEATURE_POINT_HIGHLIGHT_COLOR);
                featurePointTypes.put(
                        featurePoints.get(i).getFeaturePointType(),
                        weightedFeaturePoints.getSize(i)
                );
            }
        } else {
            getSecondaryFeaturePoints().resetAllColorsToDefault();
            weightedFeaturePoints.resetAllColorsToDefault();
            featurePointTypes.clear();
        }
    }
    
    /**
     * Changes the color of the secondary face's feature point at the given index
     * and of its weighted representation when the cursor hovers over the feature point's name.
     * The index is received as the data payload of {@code actionEvent}.
     * 
     * @param actionEvent Action event with the index of the feature point as its payload data
     * @param entered {@code true} if the cursor entered the feature point,
     *                {@code false} if the cursor left the feature point
     */
    private void hoverFeaturePoint(LoadedActionEvent actionEvent, boolean entered) {
        final int index = (int) actionEvent.getData();
        
        if (entered) { // entering a feature point
            colorSecondaryFaceFeaturePoint(index, FEATURE_POINT_HOVER_COLOR);
            hoveredFeaturePoint = getTypeOfFeaturePoint(index);
        } else if (featurePointTypes.containsKey(hoveredFeaturePoint)) { // leaving highlighted FP
            colorSecondaryFaceFeaturePoint(index, FEATURE_POINT_HIGHLIGHT_COLOR);
        } else { // leaving ordinary FP
            resetSecondaryFaceFeaturePointColor(index);
        }
    }

    /**
     * Sets the color of the secondary face's feature point at the given index
     * and of its weighted representation.
     * 
     * @param index Index of the feature point
     * @param color New color of the feature point
     */
    private void colorSecondaryFaceFeaturePoint(int index, Color color) {
        getSecondaryFeaturePoints().setColor(index, color);
        weightedFeaturePoints.setColor(index, color);
    }

    /**
     * Resets to default the color of the secondary face's feature point at the given index
     * and of its weighted representation.
     * 
     * @param index Index of the feature point
     */
    private void resetSecondaryFaceFeaturePointColor(final int index) {
        getSecondaryFeaturePoints().resetColorToDefault(index);
        weightedFeaturePoints.resetColorToDefault(index);
    }

    /**
     * Changes the size of the secondary face's feature point at the given index.
     * The index is received as the data payload of {@code actionEvent}.
     * 
     * @param actionEvent Action event with the index of the feature point as its payload data
     */
    private void resizeFeaturePoint(LoadedActionEvent actionEvent) {
        final int index = (int) actionEvent.getData();
        final double size = ControlPanelBuilder.parseLocaleDouble((JTextField) actionEvent.getSource());
        
        weightedFeaturePoints.setSize(index, size);
        featurePointTypes.replace(getTypeOfFeaturePoint(index), size);
    }
    
    /**
     * Returns type of the feature point at the given index in the secondary face.
     * 
     * @param index Index of the feature point
     * @return Type of the feature point or {@code null}
     */
    private FeaturePointType getTypeOfFeaturePoint(int index) {
        final List<FeaturePoint> featurePoints = getSecondaryFeaturePoints().getFeaturePoints();
        if (index < 0 || index >= featurePoints.size()) {
            return null;
        }
        
        return featurePoints.get(index)
                .getFeaturePointType();
    }
}
