// License: GPL. For details, see LICENSE file.
package org.openstreetmap.josm.gui.widgets;

import java.awt.Color;
import java.awt.Component;
import java.awt.Cursor;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.event.MouseEvent;
import java.text.MessageFormat;
import java.util.Arrays;

import javax.annotation.Nullable;
import javax.swing.JLabel;
import javax.swing.JTree;
import javax.swing.event.HyperlinkEvent;
import javax.swing.event.MouseInputListener;
import javax.swing.text.html.HTMLDocument;
import javax.swing.text.html.StyleSheet;
import javax.swing.tree.DefaultMutableTreeNode;
import javax.swing.tree.DefaultTreeCellRenderer;
import javax.swing.tree.TreePath;

import org.openstreetmap.josm.tools.ColorHelper;
import org.openstreetmap.josm.tools.OpenBrowser;

/**
 * A {@link javax.swing.tree.TreeCellRenderer} for cells that may contain hyperlinks.
 * @since xxx
 */
public class HtmlTreeCellRenderer extends DefaultTreeCellRenderer {
    /**
     * This only exists since the JTree cannot pass events to subcomponents.
     */
    private static class JTreeMouseInputListener implements MouseInputListener {
        @Override
        public void mouseClicked(MouseEvent mouseEvent) {
            dispatchEvent(mouseEvent);
        }

        @Override
        public void mouseMoved(MouseEvent mouseEvent) {
            dispatchEvent(mouseEvent);
        }

        @Override
        public void mousePressed(MouseEvent mouseEvent) {
            // Do nothing -- this can cause issues with the popup menu
        }

        @Override
        public void mouseReleased(MouseEvent mouseEvent) {
            // Do nothing -- this can cause issues with the popup menu
        }

        @Override
        public void mouseEntered(MouseEvent mouseEvent) {
            dispatchEvent(mouseEvent);
        }

        @Override
        public void mouseExited(MouseEvent mouseEvent) {
            dispatchEvent(mouseEvent);
        }

        @Override
        public void mouseDragged(MouseEvent mouseEvent) {
            dispatchEvent(mouseEvent);
        }

        /**
         * Dispatch mouse events for HTML-like cells
         * @param mouseEvent The event to dispatch
         */
        private static void dispatchEvent(MouseEvent mouseEvent) {
            if (mouseEvent.getComponent() instanceof JTree) {
                final Point p = mouseEvent.getPoint();
                final JTree tree = (JTree) mouseEvent.getComponent();
                final Component component = getComponentAt(tree, p); // p is translated here
                if (component != null) {
                    component.dispatchEvent(new MouseEvent(component, // Use the component we found to fire the event
                            mouseEvent.getID(), mouseEvent.getWhen(), mouseEvent.getModifiers(),
                            p.x, p.y, mouseEvent.getClickCount(), mouseEvent.isPopupTrigger(),
                            mouseEvent.getButton()));
                    component.setBounds(0, 0, 0, 0);
                }
            }
        }

        /**
         * Get a rendered component for HTML
         * @param tree The tree to get the component from
         * @param p The point to get the component at (will be modified)
         * @return The component
         */
        @Nullable
        private static Component getComponentAt(JTree tree, Point p) {
            final TreePath path = tree.getPathForLocation(p.x, p.y);
            if (path != null && tree.getCellRenderer() instanceof HtmlTreeCellRenderer) {
                final HtmlTreeCellRenderer renderer = (HtmlTreeCellRenderer) tree.getCellRenderer();
                final JosmEditorPane panel = renderer.panel;
                final int row = tree.getRowForPath(path);
                final Object last = path.getLastPathComponent();
                // We need to get the right text into the panel, so call the renderer
                final Component component = renderer.getTreeCellRendererComponent(tree, last,
                        tree.isRowSelected(row), tree.isExpanded(row),
                        tree.getModel().isLeaf(last), row, true);
                final Rectangle bounds = tree.getPathBounds(path);
                if (bounds != null) {
                    // If we don't translate the point, we are attempting to get the link in the wrong frame of reference
                    // The reference x/y are 0. This is set in BasicTextUI#getVisibleEditorRect, so we must translate the point here.
                    final int width = (component instanceof JLabel)
                            ? ((JLabel) component).getIcon().getIconWidth() + ((JLabel) component).getIconTextGap()
                            : 0;
                    // This translation puts the origin point at the upper-left of the JLabel, but moves it to the right of the icon to the text
                    // Yes, it is - width, not + width. If you change this, make certain that clicking links still works!
                    p.translate(-bounds.x - width, -bounds.y);
                    // Just make certain that we are consistent for other objects.
                    bounds.x = 0;
                    bounds.y = 0;
                    // Set the correct width
                    bounds.width -= width;
                    // Set the bounds from the JTree component (needed for proper layout)
                    panel.setBounds(bounds);
                    return panel;
                }
            }
            return null;
        }
    }

    private static final long serialVersionUID = -4842755204541968238L;
    /**
     * A reusable panel to avoid new objects where possible.
     * It isn't worth it to make a new object for each row, since it doesn't receive mouse events (tested on Java 8).
     */
    private final JosmEditorPane panel = new JosmEditorPane();
    /** JTree does not send mouse events to subcomponents */
    private final JTreeMouseInputListener mouseTreeListener = new JTreeMouseInputListener();
    /** The tree used to set cursors when entering/leaving a hyperlink */
    private JTree tree;

    /**
     * Create a new renderer
     */
    public HtmlTreeCellRenderer() {
        panel.addHyperlinkListener(e -> {
            if (e.getURL() != null) {
                if (HyperlinkEvent.EventType.ACTIVATED.equals(e.getEventType())) {
                    OpenBrowser.displayUrl(e.getURL().toString());
                } else if (this.tree != null && HyperlinkEvent.EventType.ENTERED.equals(e.getEventType())) {
                    this.tree.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR));
                } else if (this.tree != null && HyperlinkEvent.EventType.EXITED.equals(e.getEventType())) {
                    this.tree.setCursor(Cursor.getDefaultCursor());
                }
            }
        });
        JosmEditorPane.makeJLabelLike(panel, false);
        final Color defaultLink = ColorHelper.html2color(JosmEditorPane.getLinkColor());
        final Color lighterLink = defaultLink.brighter().brighter();
        final Color darkerLink = defaultLink.darker().darker();
        final String divRule = "{0} '{'background-color:{1}; color:{2};'}'";
        final String aRule = "{0} a '{'text-decoration: underline; color:{1}'}'";
        StyleSheet ss = ((HTMLDocument) panel.getDocument()).getStyleSheet();
        ss.addRule(MessageFormat.format(divRule, ".selected", ColorHelper.color2html(getBackgroundSelectionColor()),
                ColorHelper.color2html(getTextSelectionColor())));
        ss.addRule(MessageFormat.format(aRule, ".selected",
                ColorHelper.color2html(getAppropriateColor(getBackgroundSelectionColor(), defaultLink, lighterLink, darkerLink))));
        ss.addRule(MessageFormat.format(divRule, ".not-selected", ColorHelper.color2html(getBackgroundNonSelectionColor()),
                ColorHelper.color2html(getTextNonSelectionColor())));
        ss.addRule(MessageFormat.format(aRule, ".not-selected",
                ColorHelper.color2html(getAppropriateColor(getBackgroundNonSelectionColor(), defaultLink, lighterLink, darkerLink))));
    }

    @Override
    public Component getTreeCellRendererComponent(JTree tree, Object value, boolean sel, boolean expanded,
                                                  boolean leaf, int row, boolean hasFocus) {
        final Component component = super.getTreeCellRendererComponent(tree, value, sel, expanded, leaf, row, hasFocus);
        this.tree = tree;
        if (value instanceof DefaultMutableTreeNode) {
            if (Arrays.stream(tree.getMouseListeners()).noneMatch(this.mouseTreeListener::equals)) {
                tree.addMouseListener(this.mouseTreeListener);
                tree.addMouseMotionListener(this.mouseTreeListener);
            }
            final DefaultMutableTreeNode node = (DefaultMutableTreeNode) value;
            final Object object = node.getUserObject();
            if (object instanceof String && ((String) object).startsWith("<html>") && ((String) object).endsWith("</html>")) {
                final String text = (String) object;
                final String modified = "<html><div class=\"" + (sel ? "selected" : "not-selected") + "\">"
                        + text.substring("<html>".length(), text.length() - "</html>".length()) + "</div></html>";
                this.panel.setText(modified);
                this.setText(this.panel.getText()); // This gets the custom CSS into the JLabel
            }
        }
        return component;
    }

    /**
     * Convert the rgb value for use in a relative luminance calculation
     * @param rgb The value to convert
     * @return The appropriate value to use in the calculation
     */
    private static double convertRGB(int rgb) {
        final double normalized = rgb / 255d;
        if (normalized <= 0.03928) {
            return normalized / 12.92;
        }
        return Math.pow(((normalized + 0.055)/1.055), 2.4);
    }

    /**
     * Calculate the contrast ratio between two luminance values
     * @param luminanceOne The first luminance value
     * @param luminanceTwo The second luminance value
     * @return The contrast ratio. Higher is usually better.
     */
    private static double contrastRatio(double luminanceOne, double luminanceTwo) {
        final double min = Math.min(luminanceOne, luminanceTwo);
        final double max = Math.max(luminanceOne, luminanceTwo);
        return (max + 0.05) / (min + 0.05);
    }

    /**
     * Calculate the luminance of a color
     * @param red The red part
     * @param green The green part
     * @param blue The blue part
     * @return The relative luminance (0-1, 0 black, 1 white)
     */
    private static double luminance(int red, int green, int blue) {
        return 0.2126 * convertRGB(red)
                + 0.7152 * convertRGB(green)
                + 0.0722 * convertRGB(blue);
    }

    /**
     * Get the appropriate color given a background
     * @param background The background color
     * @param options The options to choose from
     * @return The appropriate color
     */
    private static Color getAppropriateColor(Color background, Color... options) {
        if (options.length < 2) {
            throw new IllegalArgumentException("There must be at least two color options");
        }
        // sRGB calculation
        final double backgroundLuminance = luminance(background.getRed(), background.getGreen(), background.getBlue());
        double lastContrastRatio = 0; // W3 recommends a contrast ratio of at least 4.5:1. We should be shooting for 7:1.
        Color lastColor = null;
        for (Color option : options) {
            final double contrastRatio = contrastRatio(backgroundLuminance, luminance(option.getRed(), option.getGreen(), option.getBlue()));
            if (contrastRatio > lastContrastRatio) {
                lastContrastRatio = contrastRatio;
                lastColor = option;
            }
        }
        return lastColor;
    }
}
