/*
 * The contents of this file are subject to the terms of the Common Development
 * and Distribution License (the License). You may not use this file except in
 * compliance with the License.
 *
 * You can obtain a copy of the License at http://www.netbeans.org/cddl.html
 * or http://www.netbeans.org/cddl.txt.
 * 
 * When distributing Covered Code, include this CDDL Header Notice in each file
 * and include the License file at http://www.netbeans.org/cddl.txt.
 * If applicable, add the following below the CDDL Header, with the fields
 * enclosed by brackets [] replaced by your own identifying information:
 * "Portions Copyrighted [year] [name of copyright owner]"
 *
 * The Original Software is NetBeans. The Initial Developer of the Original
 * Software is Sun Microsystems, Inc. Portions Copyright 1997-2006 Sun
 * Microsystems, Inc. All Rights Reserved.
 */
package org.openide.awt;

import org.openide.awt.HtmlLabelUI;
import org.openide.awt.HtmlRenderer;

import java.awt.*;
import java.awt.event.*;
import java.awt.image.BufferedImage;

import java.beans.PropertyChangeListener;
import java.beans.VetoableChangeListener;

import java.io.*;

import java.lang.ref.Reference;
import java.lang.ref.SoftReference;

import javax.swing.*;
import javax.swing.border.Border;
import javax.swing.event.AncestorListener;
import javax.swing.plaf.LabelUI;
import javax.swing.plaf.basic.BasicLabelUI;


/**
 * Html renderer component implementation.  The actual painting is done by HtmlLabelUI, which uses
 * HtmlRenderer.renderString().  What this class does:   Provide some methods for resetting its state
 * between uses (see HtmlRenderer.createLabel() for why), overrides for a bunch of things for performance
 * reasons, and some conversions to handle the case that the lightweight html renderer is disabled
 * (-J-Dnb.useSwingHtmlRendering=true), to convert our minor extensions to html syntax to standard
 * syntax for the swing renderer.
 * <p>
 * Mainly this class provides an implementation of the various cell renderer interfaces which
 * HtmlRenderer.Renderer aggregates, and the convenience methods it provides.
 *
 * @author Tim Boudreau
 * @since 4.30
 *
 */
class HtmlRendererImpl extends JLabel implements HtmlRenderer.Renderer {
    private static final Rectangle bounds = new Rectangle();
    private static final boolean swingRendering = Boolean.getBoolean("nb.useSwingHtmlRendering"); //NOI18N
    private static final Insets EMPTY_INSETS = new Insets(0, 0, 0, 0);
    static final int TYPE_UNKNOWN = -1;
    static final int TYPE_TREE = 0;
    static final int TYPE_LIST = 1;
    static final int TYPE_TABLE = 2;

    //For experimentation - holding the graphics object may be the source of some
    //strange painting problems on Apple
    private static boolean noCacheGraphics = Boolean.getBoolean("nb.renderer.nocache"); //NOI18N
    private static Reference scratchGraphics = null;
    private boolean centered = false;
    private boolean parentFocused = false;
    private Boolean html = null;
    private int indent = 0;
    private Border border = null;
    private boolean selected = false;
    private boolean leadSelection = false;
    private Dimension prefSize = null;
    private int type = TYPE_UNKNOWN;
    private int renderStyle = HtmlRenderer.STYLE_CLIP;
    private boolean enabled = true;

    /** Restore the renderer to a pristine state */
    public void reset() {
        parentFocused = false;
        setCentered(false);
        html = null;
        indent = 0;
        border = null;
        setIcon(null);
        setOpaque(false);
        selected = false;
        leadSelection = false;
        prefSize = null;
        type = TYPE_UNKNOWN;
        renderStyle = HtmlRenderer.STYLE_CLIP;
        setFont(UIManager.getFont("controlFont")); //NOI18N
        setIconTextGap(3);
        setEnabled(true);
        border = null;

        //Defensively ensure the insets haven't been messed with
        EMPTY_INSETS.top = 0;
        EMPTY_INSETS.left = 0;
        EMPTY_INSETS.right = 0;
        EMPTY_INSETS.bottom = 0;
    }

    public Component getTableCellRendererComponent(
        JTable table, Object value, boolean selected, boolean leadSelection, int row, int column
    ) {
        reset();
        configureFrom(value, table, selected, leadSelection);
        type = TYPE_TABLE;

        if (swingRendering && selected) {
            setBackground(table.getSelectionBackground());
            setForeground(table.getSelectionForeground());
            setOpaque(true);
        }

        return this;
    }

    public Component getTreeCellRendererComponent(
        JTree tree, Object value, boolean selected, boolean expanded, boolean leaf, int row, boolean leadSelection
    ) {
        reset();
        configureFrom(value, tree, selected, leadSelection);
        type = TYPE_TREE;

        if (swingRendering && selected) {
            if (HtmlLabelUI.GTK) {
                setBackground(HtmlLabelUI.getBackgroundFor(this));
                setForeground(HtmlLabelUI.getForegroundFor(this));
            }
            setOpaque(true);
        }

        return this;
    }

    public Component getListCellRendererComponent(
        JList list, Object value, int index, boolean selected, boolean leadSelection
    ) {
        reset();
        configureFrom(value, list, selected, leadSelection);
        type = TYPE_LIST;

        if (swingRendering && selected) {
            setBackground(list.getSelectionBackground());
            setForeground(list.getSelectionForeground());
            setOpaque(true);
        }

        return this;
    }

    /** Generic code to set properties appropriately from any of the renderer
     * fetching methods */
    private void configureFrom(Object value, JComponent target, boolean selected, boolean leadSelection) {
        if (value == null) {
            value = "";
        }

        setText((value == null) ? "" : value.toString());

        setSelected(selected);

        if (selected) {
            setParentFocused(checkFocused(target));
        } else {
            setParentFocused(false);
        }

        setEnabled(target.isEnabled());

        setLeadSelection(leadSelection);

        setFont(target.getFont());
    }

    private boolean checkFocused(JComponent c) {
        Component focused = KeyboardFocusManager.getCurrentKeyboardFocusManager().getPermanentFocusOwner();
        boolean result = c == focused;

        if (!result) {
            result = c.isAncestorOf(focused);
        }

        return result;
    }

    public void addNotify() {
        if (swingRendering) {
            super.addNotify();
        }
    }

    public void removeNotify() {
        if (swingRendering) {
            super.removeNotify();
        }
    }

    public void setSelected(boolean val) {
        selected = val;
    }

    public void setParentFocused(boolean val) {
        parentFocused = val;
    }

    public void setLeadSelection(boolean val) {
        leadSelection = val;
    }

    public void setCentered(boolean val) {
        centered = val;

        if (val) {
            setIconTextGap(5);
        }

        if (swingRendering) {
            if (val) {
                setVerticalTextPosition(JLabel.BOTTOM);
                setHorizontalAlignment(JLabel.CENTER);
                setHorizontalTextPosition(JLabel.CENTER);
            } else {
                setVerticalTextPosition(JLabel.CENTER);
                setHorizontalAlignment(JLabel.LEADING);
                setHorizontalTextPosition(JLabel.TRAILING);
            }
        }
    }

    public void setIndent(int pixels) {
        this.indent = pixels;
    }

    public void setHtml(boolean val) {
        Boolean wasHtml = html;
        String txt = getText();
        html = val ? Boolean.TRUE : Boolean.FALSE;

        if (swingRendering && (html != wasHtml)) {
            //Ensure label UI gets updated and builds its little document tree...
            firePropertyChange("text", txt, getText()); //NOI18N
        }
    }

    public void setRenderStyle(int style) {
        renderStyle = style;
    }

    int getRenderStyle() {
        return renderStyle;
    }

    boolean isLeadSelection() {
        return leadSelection;
    }

    boolean isCentered() {
        return centered;
    }

    boolean isParentFocused() {
        return parentFocused;
    }

    boolean isHtml() {
        if (html == null) {
            String s = getText();
            html = checkHtml(s);
        }

        return html.booleanValue();
    }

    private Boolean checkHtml(String s) {
        Boolean result;

        if (s == null) {
            result = Boolean.FALSE;
        } else if (s.startsWith("<html") || s.startsWith("<HTML")) { //NOI18N
            result = Boolean.TRUE;
        } else {
            result = Boolean.FALSE;
        }

        return result;
    }

    boolean isSelected() {
        return selected;
    }

    int getIndent() {
        return indent;
    }

    int getType() {
        return type;
    }

    public Dimension getPreferredSize() {
        if (!swingRendering) {
            if (prefSize == null) {
                prefSize = getUI().getPreferredSize(this);
            }

            return prefSize;
        } else {
            return super.getPreferredSize();
        }
    }

    /**
     * Overridden for the case that we're running with the lightweight html renderer disabled, to convert
     * any less-than-legal html to legal html for purposes of Swing's html rendering.
     *
     * @return The text - unless the renderer is disabled, this just return super.getText()
     */
    public String getText() {
        String result = super.getText();

        if (swingRendering && Boolean.TRUE.equals(html)) {
            //Standard swing rendering needs an opening HTML tag to function, so make sure there is
            //one if we're not using HtmlLabelUI
            result = ensureHtmlTags(result);
        } else if (swingRendering && (html == null)) {
            //Cannot call isHtml() here, it will create an endless loop, so manually check the HTML status
            html = checkHtml(super.getText());

            if (Boolean.TRUE.equals(html)) {
                result = ensureHtmlTags(result);
            }
        }

        return result;
    }

    /**
     * Converts our extended html syntax (allowing UIManager color keys and omitting opening html tags
     * into standard html.  Only called if the lightweight html renderer is disabled and we're running with
     * a standard JLabel UI
     *
     * @param s The string that is the text of the label
     * @return The same string converted to standard HTML Swing's rendering infrastructure will know what to do
     *         with
     */
    private String ensureHtmlTags(String s) {
        s = ensureLegalFontColorTags(s);

        if (!s.startsWith("<HTML") && !s.startsWith("<html")) { //NOI18N
            s = "<html>" + s + "</html>"; //NOI18N
        }

        return s;
    }

    /**
     * Converts extended UI manager color tags into legal html in the case that we're using swing rendering
     *
     * @param s string to convert if it has questionable font tags
     * @return The converted string
     */
    private static String ensureLegalFontColorTags(String s) {
        String check = s.toUpperCase();
        int start = 0;
        int fidx = check.indexOf("<FONT", start); //NOI18N
        StringBuffer sb = null;

        if ((fidx != -1) && (fidx <= s.length())) {
            while ((fidx != -1) && (fidx <= s.length())) {
                int cidx = check.indexOf("COLOR", start); //NOI18N
                int tagEnd = check.indexOf('>', start); //NOI18N
                start = tagEnd + 1;

                if (tagEnd == -1) {
                    break;
                }

                if (cidx != -1) {
                    if (cidx < tagEnd) {
                        //we have a font color tag
                        int eidx = check.indexOf('=', cidx); //NOI18N

                        if (eidx != -1) {
                            int bangIdx = check.indexOf('!', eidx); //NOI18N

                            if ((bangIdx != -1) && (bangIdx < tagEnd)) {
                                int colorStart = bangIdx + 1;
                                int colorEnd = tagEnd;

                                for (int i = colorStart; i < tagEnd; i++) {
                                    char c = s.charAt(i);

                                    if (!Character.isLetter(c)) {
                                        colorEnd = i;

                                        break;
                                    }
                                }

                                if (sb == null) {
                                    sb = new StringBuffer(s);
                                }

                                String colorString = s.substring(colorStart, colorEnd);
                                String converted = convertToStandardColor(colorString);
                                sb.replace(bangIdx, colorEnd, converted);
                                s = sb.toString();
                                check = s.toUpperCase();
                            }
                        }
                    }
                }

                fidx = check.indexOf("<FONT", start); //NOI18N
                start = fidx;
            }
        }

        if (sb != null) {
            return sb.toString();
        } else {
            return s;
        }
    }

    /**
     * Creates a standard html #nnnnnn string from a string representing a UIManager key.  If the color is not found,
     * black will be used.  Only used if the lightweight html renderer is disabled.
     *
     * @param colorString  A string found after a ! character in a color definition, which needs to be converted to
     *        standard HTML
     * @return A hex number string
     */
    private static String convertToStandardColor(String colorString) {
        Color c = UIManager.getColor(colorString);

        if (c == null) {
            c = Color.BLACK;
        }

        StringBuffer sb = new StringBuffer(7);
        sb.append('#');
        sb.append(hexString(c.getRed()));
        sb.append(hexString(c.getGreen()));
        sb.append(hexString(c.getBlue()));

        return sb.toString();
    }

    /**
     * Gets a hex string for an integer.  Ensures the result is always two characters long, which is not
     * true of Integer.toHexString().
     *
     * @param r an integer < 255
     * @return a 2 character hexadecimal string
     */
    private static String hexString(int r) {
        String s = Integer.toHexString(r);

        if (s.length() == 1) {
            s = '0' + s;
        }

        return s;
    }

    /** Overridden to do nothing under normal circumstances.  If the boolean flag to <strong>not</strong> use the
     * internal HTML renderer is in effect, this will fire changes normally */
    protected final void firePropertyChange(String name, Object old, Object nue) {
        if (swingRendering) {
            if ("text".equals(name) && isHtml()) {
                //Force in the HTML tags so the UI will set up swing HTML rendering appropriately
                nue = getText();
            }

            super.firePropertyChange(name, old, nue);
        }
    }

    public Border getBorder() {
        Border result;

        if ((indent != 0) && swingRendering) {
            result = BorderFactory.createEmptyBorder(0, indent, 0, 0);
        } else {
            result = border;
        }

        return result;
    }

    public void setBorder(Border b) {
        Border old = border;
        border = b;

        if (swingRendering) {
            firePropertyChange("border", old, b);
        }
    }

    public Insets getInsets() {
        Insets result;

        //Call getBorder(), not just read the field - if swingRendering, the border will be constructed, and the
        //insets are what will make the indent property work;  HtmlLabelUI doesn't need this, it just reads the
        //insets property, but BasicLabelUI and its ilk do
        Border b = getBorder();

        if (b == null) {
            result = EMPTY_INSETS;
        } else {
            result = b.getBorderInsets(this);
        }

        return result;
    }

    public void setEnabled(boolean b) {
        //OptimizeIt shows about 12Ms overhead calling back to Component.enable(), so avoid it if possible
        enabled = b;

        if (swingRendering) {
            super.setEnabled(b);
        }
    }

    public boolean isEnabled() {
        return enabled;
    }

    public void updateUI() {
        if (swingRendering) {
            super.updateUI();
        } else {
            setUI(HtmlLabelUI.createUI(this));
        }
    }

    /** Overridden to produce a graphics object even when isDisplayable() is
     * false, so that calls to getPreferredSize() will return accurate
     * dimensions (presuming the font and text are set correctly) even when
     * not onscreen. */
    public Graphics getGraphics() {
        Graphics result = null;

        if (isDisplayable()) {
            result = super.getGraphics();
        }

        if (result == null) {
            result = scratchGraphics();
        }

        return result;
    }

    /** Fetch a scratch graphics object for calculating preferred sizes while
     * offscreen */
    private static final Graphics scratchGraphics() {
        Graphics result = null;

        if (scratchGraphics != null) {
            result = (Graphics) scratchGraphics.get();

            if (result != null) {
                result.setClip(null); //just in case somebody did something nasty
            }
        }

        if (result == null) {
            result = GraphicsEnvironment.getLocalGraphicsEnvironment().getDefaultScreenDevice().getDefaultConfiguration()
                                        .createCompatibleImage(1, 1).getGraphics();

            if (!noCacheGraphics) {
                scratchGraphics = new SoftReference(result);
            }
        }

        return result;
    }

    public void setBounds(int x, int y, int w, int h) {
        if (swingRendering) {
            super.setBounds(x, y, w, h);
        }

        bounds.setBounds(x, y, w, h);
    }

    public void reshape(int x, int y, int w, int h) {
        if (swingRendering) {
            super.reshape(x, y, w, h);
        }
    }

    public int getWidth() {
        return bounds.width;
    }

    public int getHeight() {
        return bounds.height;
    }

    public Point getLocation() {
        return bounds.getLocation();
    }

    /** Overridden to do nothing for performance reasons */
    public void validate() {
        //do nothing
    }

    /** Overridden to do nothing for performance reasons */
    public void repaint(long tm, int x, int y, int w, int h) {
        //do nothing
    }

    /** Overridden to do nothing for performance reasons */
    public void repaint() {
        //do nothing
    }

    /** Overridden to do nothing for performance reasons */
    public void invalidate() {
        //do nothing
    }

    /** Overridden to do nothing for performance reasons */
    public void revalidate() {
        //do nothing
    }

    /** Overridden to do nothing for performance reasons */
    public void addAncestorListener(AncestorListener l) {
        if (swingRendering) {
            super.addAncestorListener(l);
        }
    }

    /** Overridden to do nothing for performance reasons */
    public void addComponentListener(ComponentListener l) {
        if (swingRendering) {
            super.addComponentListener(l);
        }
    }

    /** Overridden to do nothing for performance reasons */
    public void addContainerListener(ContainerListener l) {
        if (swingRendering) {
            super.addContainerListener(l);
        }
    }

    /** Overridden to do nothing for performance reasons */
    public void addHierarchyListener(HierarchyListener l) {
        if (swingRendering) {
            super.addHierarchyListener(l);
        }
    }

    /** Overridden to do nothing for performance reasons */
    public void addHierarchyBoundsListener(HierarchyBoundsListener l) {
        if (swingRendering) {
            super.addHierarchyBoundsListener(l);
        }
    }

    /** Overridden to do nothing for performance reasons */
    public void addInputMethodListener(InputMethodListener l) {
        if (swingRendering) {
            super.addInputMethodListener(l);
        }
    }

    /** Overridden to do nothing for performance reasons */
    public void addFocusListener(FocusListener fl) {
        if (swingRendering) {
            super.addFocusListener(fl);
        }
    }

    /** Overridden to do nothing for performance reasons */
    public void addMouseListener(MouseListener ml) {
        if (swingRendering) {
            super.addMouseListener(ml);
        }
    }

    /** Overridden to do nothing for performance reasons */
    public void addMouseWheelListener(MouseWheelListener ml) {
        if (swingRendering) {
            super.addMouseWheelListener(ml);
        }
    }

    /** Overridden to do nothing for performance reasons */
    public void addMouseMotionListener(MouseMotionListener ml) {
        if (swingRendering) {
            super.addMouseMotionListener(ml);
        }
    }

    /** Overridden to do nothing for performance reasons */
    public void addVetoableChangeListener(VetoableChangeListener vl) {
        if (swingRendering) {
            super.addVetoableChangeListener(vl);
        }
    }

    /** Overridden to do nothing for performance reasons, unless using standard swing rendering */
    public void addPropertyChangeListener(String s, PropertyChangeListener l) {
        if (swingRendering) {
            super.addPropertyChangeListener(s, l);
        }
    }

    public void addPropertyChangeListener(PropertyChangeListener l) {
        if (swingRendering) {
            super.addPropertyChangeListener(l);
        }
    }
}
