package cz.fidentis.analyst.distance;

import cz.fidentis.analyst.core.ControlPanel;
import cz.fidentis.analyst.core.ControlPanelBuilder;
import cz.fidentis.analyst.core.LoadedActionEvent;
import cz.fidentis.analyst.core.TopControlPanel;
import cz.fidentis.analyst.feature.FeaturePoint;
import cz.fidentis.analyst.feature.FeaturePointType;
import cz.fidentis.analyst.scene.DrawableFeaturePoints;
import cz.fidentis.analyst.symmetry.SymmetryPanel;
import java.awt.Dimension;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.KeyEvent;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.util.ArrayList;
import java.util.DoubleSummaryStatistics;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.swing.AbstractAction;
import javax.swing.Action;
import javax.swing.BorderFactory;
import javax.swing.ImageIcon;
import javax.swing.JCheckBox;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JTextField;
import javax.swing.KeyStroke;

/**
 * Control panel for Hausdorff distance.
 * 
 * @author Radek Oslejsek
 * @author Daniel Schramm
 */
public class DistancePanel extends ControlPanel {
    
    /*
     * Mandatory design elements
     */
    public static final String ICON = "distance28x28.png";
    public static final String NAME = "Hausdorff distance";
    
    /*
     * External actions
     */
    public static final String ACTION_COMMAND_SET_DISPLAYED_HEATMAP = "set displayed heatmap";
    public static final String ACTION_COMMAND_SET_DISTANCE_STRATEGY = "set strategy";
    public static final String ACTION_COMMAND_RELATIVE_ABSOLUTE_DIST = "switch abosulte-relative distance";
    public static final String ACTION_COMMAND_FEATURE_POINT_HOVER_IN = "highlight hovered FP";
    public static final String ACTION_COMMAND_FEATURE_POINT_HOVER_OUT = "set default color of hovered FP";
    public static final String ACTION_COMMAND_FEATURE_POINT_HIGHLIGHT = "highlight feature point with color";
    public static final String ACTION_COMMAND_FEATURE_POINT_SELECT_ALL = "select all feature points";
    public static final String ACTION_COMMAND_FEATURE_POINT_SELECT_NONE = "deselect all feature points";
    public static final String ACTION_COMMAND_FEATURE_POINT_RESIZE = "set size of feature point";
    public static final String ACTION_COMMAND_DISTANCE_RECOMPUTE = "recompute the Hausdorff distance";
    public static final String ACTION_COMMAND_SHOW_HIDE_WEIGHTED_FPOINTS = "show-hide weighted feature points";
    
    /*
     * Configuration of panel-specific GUI elements
     */
    public static final String STRATEGY_POINT_TO_POINT = "Point to point";
    public static final String STRATEGY_POINT_TO_TRIANGLE = "Point to triangle";
    public static final String HEATMAP_HAUSDORFF_DISTANCE = "Hausdorff distance";
    public static final String HEATMAP_WEIGHTED_HAUSDORFF_DISTANCE = "Weighted Hausdorff distance";
    public static final String HEATMAP_HIDE = "None";
    
    private final Map<FeaturePointType, JLabel> featurePointStats;
    private final List<JCheckBox> featurePointCheckBoxes;
    private final JLabel avgHD, avgHDWeight, maxHD, maxHDWeight, minHD, minHDWeight;
    
    /**
     * Constructor.
     * 
     * @param action Action listener
     * @param featurePoints List of all feature points which can be used to calculate
     *                      the weighted Hausdorff distance
     * @param selectedFPs Set of feature point types which are initially selected
     *                    to be used to calculate the weighted Hausdorff distance
     */
    public DistancePanel(ActionListener action, List<FeaturePoint> featurePoints, Set<FeaturePointType> selectedFPs) {
        this.setName(NAME);
        
        final ControlPanelBuilder builder = new ControlPanelBuilder(this);
        
        builder.addCaptionLine("Computation options:");
        builder.addLine();
        
        builder.addCheckBoxOptionLine(
                null, 
                "Relative distance", 
                false,
                createListener(action, ACTION_COMMAND_RELATIVE_ABSOLUTE_DIST)
        );
        builder.addLine();
        
        builder.addOptionText("Distance strategy");
        builder.addComboBox(
                List.of(STRATEGY_POINT_TO_POINT, STRATEGY_POINT_TO_TRIANGLE),
                createListener(action, ACTION_COMMAND_SET_DISTANCE_STRATEGY)
        );
        builder.addGap();
        builder.addLine();
        
        final JPanel featurePointsPanel = new JPanel();
        final ControlPanelBuilder fpBuilder = new ControlPanelBuilder(featurePointsPanel);
        featurePointStats = new HashMap<>(featurePoints.size());
        featurePointCheckBoxes = new ArrayList<>(featurePoints.size());
        for (int i = 0; i < featurePoints.size(); i++) {
            final FeaturePoint featurePoint = featurePoints.get(i);
            final FeaturePointType featurePointType = featurePoint.getFeaturePointType();
            
            final JCheckBox checkBox = fpBuilder.addCheckBox(
                    selectedFPs.contains(featurePointType),
                    createListener(action, ACTION_COMMAND_FEATURE_POINT_HIGHLIGHT, i)
            );
            checkBox.setText(featurePointType.getName());
            final int index = i;
            checkBox.addMouseListener(new MouseAdapter() {
                @Override
                public void mouseEntered(MouseEvent e) {
                    action.actionPerformed(new LoadedActionEvent(
                            e.getSource(),
                            ActionEvent.ACTION_PERFORMED,
                            ACTION_COMMAND_FEATURE_POINT_HOVER_IN,
                            index));
                }

                @Override
                public void mouseExited(MouseEvent e) {
                    action.actionPerformed(new LoadedActionEvent(
                            e.getSource(),
                            ActionEvent.ACTION_PERFORMED,
                            ACTION_COMMAND_FEATURE_POINT_HOVER_OUT,
                            index));
                }
            });
            featurePointCheckBoxes.add(checkBox);

            final JTextField sliderInput = fpBuilder.addSliderButtonedWithVal(100, 1, null);
            sliderInput.setText(ControlPanelBuilder.doubleToStringLocale(DrawableFeaturePoints.DEFAULT_SIZE));
            sliderInput.postActionEvent(); // Set correct position of slider
            sliderInput.addActionListener((ActionEvent ae) -> {
                if (!checkBox.isSelected()) {
                    return; // Recompute only if the feature point is selected
                }
                
                if (ControlPanelBuilder.TEXT_FIELD_BUTTON_PRESSED_MINUS.equals(ae.getActionCommand())
                        || ControlPanelBuilder.TEXT_FIELD_BUTTON_PRESSED_PLUS.equals(ae.getActionCommand())) {
                    // Plus/minus button was pressed -> recompute
                    action.actionPerformed(new ActionEvent(
                            ae.getSource(),
                            ActionEvent.ACTION_PERFORMED,
                            ACTION_COMMAND_DISTANCE_RECOMPUTE
                    ));
                }
            });
            sliderInput.addActionListener(createListener(action, ACTION_COMMAND_FEATURE_POINT_RESIZE, i));
            // Modify listener of the ENTER key press
            final Object enterKeyAction = sliderInput.getInputMap()
                    .get(KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, 0));
            final Action originalEnterAction = sliderInput.getActionMap()
                    .get(enterKeyAction);
            sliderInput.getActionMap().put(
                    enterKeyAction,
                    new AbstractAction() {
                        @Override
                        public void actionPerformed(ActionEvent e) {
                            originalEnterAction.actionPerformed(e);
                            if (!checkBox.isSelected()) {
                                return; // Recompute only if the feature point is selected
                            }
                            
                            action.actionPerformed(new ActionEvent(
                                    e.getSource(),
                                    ActionEvent.ACTION_PERFORMED,
                                    ACTION_COMMAND_DISTANCE_RECOMPUTE
                            ));
                        }
                    }
            );
            // Add mouse listener of the slider
            sliderInput.addMouseListener(new MouseAdapter() {
                private double oldValue = DrawableFeaturePoints.DEFAULT_SIZE;
                
                @Override
                public void mousePressed(MouseEvent e) {
                    oldValue = ControlPanelBuilder.parseLocaleDouble(sliderInput);
                }
                
                @Override
                public void mouseReleased(MouseEvent e) {
                    if (!checkBox.isSelected()
                            || oldValue == ControlPanelBuilder.parseLocaleDouble(sliderInput)) {
                        return; // Recompute only if the feature point is selected and value changed
                    }
                    
                    action.actionPerformed(new ActionEvent(
                            e.getSource(),
                            ActionEvent.ACTION_PERFORMED,
                            ACTION_COMMAND_DISTANCE_RECOMPUTE)
                    );
                }
            });
            
            fpBuilder.addGap();
            featurePointStats.put(featurePointType, fpBuilder.addLabelLine(null));
            fpBuilder.addGap();
            
            fpBuilder.addLine();
        }
        JScrollPane sp = builder.addScrollPane(featurePointsPanel);
        sp.setBorder(BorderFactory.createTitledBorder("Feature points"));
        sp.setPreferredSize(new Dimension(TopControlPanel.WIDTH, 200));
        builder.addLine();
        
        builder.addButton("Select all", (ActionEvent ae) -> {
            selectAllFeaturePoints(true);
            action.actionPerformed(new ActionEvent(
                    ae.getSource(),
                    ActionEvent.ACTION_PERFORMED,
                    ACTION_COMMAND_FEATURE_POINT_SELECT_ALL)
            );
        });
        builder.addButton("Deselect all", (ActionEvent ae) -> {
            selectAllFeaturePoints(false);
            action.actionPerformed(new ActionEvent(
                    ae.getSource(),
                    ActionEvent.ACTION_PERFORMED,
                    ACTION_COMMAND_FEATURE_POINT_SELECT_NONE)
            );
        });
        builder.addLine();
        
        builder.addCaptionLine("Visualization options:");
        builder.addLine();
        builder.addCheckBoxOptionLine(
                null,
                "Weighted feature points",
                true,
                createListener(action, ACTION_COMMAND_SHOW_HIDE_WEIGHTED_FPOINTS)
        );
        builder.addLine();
        
        builder.addOptionText("Heatmap");
        builder.addComboBox(
                List.of(
                        HEATMAP_HAUSDORFF_DISTANCE,
                        HEATMAP_WEIGHTED_HAUSDORFF_DISTANCE,
                        HEATMAP_HIDE
                ),
                createListener(action, ACTION_COMMAND_SET_DISPLAYED_HEATMAP)
        );
        builder.addGap();
        builder.addLine();
        
        builder.addCaptionLine("Hausdorff distance:");
        builder.addLine();
        
        builder.addOptionText("");
        builder.addOptionText("Hausdorff dist.");
        builder.addOptionText("Weighted H. dist.");
        builder.addLine();
        
        builder.addOptionText("Average");
        avgHD = builder.addLabelLine("");
        builder.addGap();
        builder.addGap();
        avgHDWeight = builder.addLabelLine("");
        builder.addLine();
        builder.addOptionText("Maximum");
        maxHD = builder.addLabelLine("");
        builder.addGap();
        builder.addGap();
        maxHDWeight = builder.addLabelLine("");
        builder.addLine();
        builder.addOptionText("Minimum");
        minHD = builder.addLabelLine("");
        builder.addGap();
        builder.addGap();
        minHDWeight = builder.addLabelLine("");
        builder.addLine();
        
        builder.addVerticalStrut();
    }
    
    @Override
    public ImageIcon getIcon() {
        return getStaticIcon();
    }
    
    /**
     * Static implementation of the {@link #getIcon()} method.
     * 
     * @return Control panel icon
     */
    public static ImageIcon getStaticIcon() {
        return new ImageIcon(SymmetryPanel.class.getClassLoader().getResource("/" + ICON));
    }
    
    /**
     * (De)selects all feature points for the computation of the weighted Hausdorff distance.
     * 
     * @param selected {@code true} if all feature point checkboxes are to be selected,
     *                 {@code false} otherwise
     */
    private void selectAllFeaturePoints(boolean selected) {
        featurePointCheckBoxes.forEach(fpCheckBox ->
            fpCheckBox.setSelected(selected)
        );
    }
    
    /**
     * Updates GUI elements that display statistical data about the calculated Hausdorff distance.
     * 
     * @param hausdorffDistance Statistical data of the ordinary Hausdorff distance
     * @param weightedHausdorffDistance Statistical data of the weighted Hausdorff distance
     */
    public void updateHausdorffDistanceStats(DoubleSummaryStatistics hausdorffDistance,
            DoubleSummaryStatistics weightedHausdorffDistance) {
        avgHD.setText(String.format("%.4f", hausdorffDistance.getAverage()));
        avgHDWeight.setText(String.format("%.4f", weightedHausdorffDistance.getAverage()));
        
        maxHD.setText(String.format("%.4f", hausdorffDistance.getMax()));
        maxHDWeight.setText(String.format("%.4f", weightedHausdorffDistance.getMax()));
        
        minHD.setText(String.format("%.4f", hausdorffDistance.getMin()));
        minHDWeight.setText(String.format("%.4f", weightedHausdorffDistance.getMin()));
    }
    
    /**
     * Updates GUI elements that display the weights of feature points
     * used to calculate the weighted Hausdorff distance.
     * 
     * @param featurePointWeights Map of feature point types and their weights
     */
    public void updateFeaturePointWeights(Map<FeaturePointType, Double> featurePointWeights) {
        featurePointStats.forEach((fpType, fpWeightLabel) -> {
            final Double fpWeight = featurePointWeights.get(fpType);
            fpWeightLabel.setText(fpWeight == null ? "" : String.format("%.3f", fpWeight));
        });
    }

}
