/*
 * Copyright (C) 2005-2006 Sun Microsystems, Inc. All rights reserved. Use is
 * subject to license terms.
 */ 

package org.jdesktop.layout;

import org.jdesktop.layout.*;
import java.awt.*;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import javax.swing.*;
import javax.swing.text.JTextComponent;
import javax.swing.text.View;
import java.util.Dictionary;
import java.util.Enumeration;
import javax.swing.border.*;
import javax.swing.plaf.metal.MetalLookAndFeel;

/**
 * Convenience class that can be used to determine the baseline of a
 * particular component.
 * <p>
 * This class is primarily useful for JREs prior to 1.6.  In 1.6 API for this
 * was added directly to Component, JComponent and the
 * appropriate ComponentUIs.  When run on a JRE of 1.6 or greater this will directly
 * call into the getBaseline method of Component.
 *
 * @author  Werner Randelshofer
 * @version $Revision$
 */
class AquaBaseline extends Baseline {
    static final AquaBaseline INSTANCE = new AquaBaseline();
    
    //
    // Used by button and label baseline code, cached to avoid excessive
    // garbage.
    //
    private static final Rectangle viewRect = new Rectangle();
    private static final Rectangle textRect = new Rectangle();
    private static final Rectangle iconRect = new Rectangle();

    // 
    // These come from TitleBorder.  NOTE that these are NOT final in
    // TitledBorder
    //
    private static final int EDGE_SPACING = 2;
    private static final int TEXT_SPACING = 2;


    private static final Insets EMPTY_INSETS = new Insets(0, 0, 0, 0);

    // Prototype label for calculating baseline of tables.
    private static JLabel TABLE_LABEL;

    // Prototype label for calculating baseline of lists.
    private static JLabel LIST_LABEL;

    // Prototype label for calculating baseline of trees.
    private static JLabel TREE_LABEL;


    /**
     * Returns the baseline for the specified component, or -1 if the
     * baseline can not be determined.  The baseline is measured from
     * the top of the component.
     *
     * @param component JComponent to calculate baseline for
     * @param width Width of the component to determine baseline for.
     * @param height Height of the component to determine baseline for.
     * @return baseline for the specified component
     */
    public int getComponentBaseline(JComponent component, int width,
            int height) {
        String uid = component.getUIClassID();
        int baseline = -1;
        if (uid == "ButtonUI" || uid == "ToggleButtonUI") {
            baseline = getButtonBaseline((AbstractButton)component,
                                         height);
        }
        else if (uid == "CheckBoxUI" || uid == "RadioButtonUI") {
            baseline = getCheckBoxBaseline((AbstractButton)component, height) + 1;
        }
        else if (uid == "ComboBoxUI") {
            return getComboBoxBaseline((JComboBox)component,
                                       height);
        }
        else if (uid == "TextAreaUI") {
            return getTextAreaBaseline((JTextArea)component, height);
        }
        else if (uid == "FormattedTextFieldUI" ||
                 uid == "PasswordFieldUI" ||
                 uid == "TextFieldUI") {
            baseline = getSingleLineTextBaseline((JTextComponent)component,
                                                 height);
        }
        else if (uid == "LabelUI") {
            baseline = getLabelBaseline((JLabel)component, height);
        }
        else if (uid == "ListUI") {
            baseline = getListBaseline((JList)component, height);
        }
        else if (uid == "PanelUI") {
            baseline = getPanelBaseline((JPanel)component, height);
        }
        else if (uid == "ProgressBarUI") {
            baseline = getProgressBarBaseline((JProgressBar)component, height);
        }
        else if (uid == "SliderUI") {
            baseline = getSliderBaseline((JSlider)component, height);
        }
        else if (uid == "SpinnerUI") {
            baseline = getSpinnerBaseline((JSpinner)component, height);
        }
        else if (uid == "ScrollPaneUI") {
            baseline = getScrollPaneBaseline((JScrollPane)component, height);
        }
        else if (uid == "TabbedPaneUI") {
            baseline = getTabbedPaneBaseline((JTabbedPane)component, height);
        }
        else if (uid == "TableUI") {
            baseline = getTableBaseline((JTable)component, height);
        }
        else if (uid == "TreeUI") {
            baseline = getTreeBaseline((JTree)component, height);
        }
        return Math.max(baseline, -1);
    }

    private static Insets rotateInsets(Insets topInsets, int targetPlacement) {
        switch(targetPlacement) {
          case JTabbedPane.LEFT:
              return new Insets(topInsets.left, topInsets.top, 
                                topInsets.right, topInsets.bottom);
          case JTabbedPane.BOTTOM:
              return new Insets(topInsets.bottom, topInsets.left,
                                topInsets.top, topInsets.right);
          case JTabbedPane.RIGHT:
              return new Insets(topInsets.left, topInsets.bottom,
                                topInsets.right, topInsets.top);
          default:
              return new Insets(topInsets.top, topInsets.left,
                                topInsets.bottom, topInsets.right);
        }
    }
    
    private int getMaxTabHeight(JTabbedPane tp) {
        int fontHeight = tp.getFontMetrics(tp.getFont()).getHeight();
        int height = fontHeight;
        boolean tallerIcons = false;
        for (int counter = tp.getTabCount() - 1; counter >= 0; counter--) {
            Icon icon = tp.getIconAt(counter);
            if (icon != null) {
                int iconHeight = icon.getIconHeight();
                height = Math.max(height, iconHeight);
                if (iconHeight > fontHeight) {
                    tallerIcons = true;
                }
            }
        }
        Insets tabInsets = UIManager.getInsets("TabbedPane.tabInsets");
        height += 2;
        /*
        if (!isMetal() || !tallerIcons) {
            height += tabInsets.top + tabInsets.bottom;
        }*/
        return height;
    }

    private int getTabbedPaneBaseline(JTabbedPane tp, int height) {
        if (tp.getTabCount() > 0) {
            Insets insets = tp.getInsets();
            Insets contentBorderInsets = UIManager.getInsets(
                "TabbedPane.contentBorderInsets");
            Insets tabAreaInsets = rotateInsets(UIManager.getInsets(
                                                 "TabbedPane.tabAreaInsets"),
                                                tp.getTabPlacement());
            FontMetrics metrics = tp.getFontMetrics(tp.getFont());
            int maxHeight = getMaxTabHeight(tp);
            iconRect.setBounds(0, 0, 0, 0);
            textRect.setBounds(0, 0, 0, 0);
            viewRect.setBounds(0, 0, Short.MAX_VALUE, maxHeight);
            SwingUtilities.layoutCompoundLabel(tp, metrics, "A", null,
                                               SwingUtilities.CENTER,
                                               SwingUtilities.CENTER,
                                               SwingUtilities.CENTER,
                                               SwingUtilities.TRAILING,
                                               viewRect,
                                               iconRect,
                                               textRect,
                                               0);
            int baseline = textRect.y + metrics.getAscent();
            switch(tp.getTabPlacement()) {
            case JTabbedPane.TOP:
                baseline += insets.top + tabAreaInsets.top + 3;
                /*
                if (isWindows()) {
                    if (tp.getTabCount() > 1) {
                        baseline += 1;
                    }
                    else {
                        baseline -= 1;
                    }
                }*/
                return baseline;
            case JTabbedPane.BOTTOM:
                baseline = tp.getHeight() - insets.bottom -
                    tabAreaInsets.bottom - maxHeight + baseline;
                /*
                if (isWindows()) {
                    if (tp.getTabCount() > 1) {
                        baseline += -1;
                    }
                    else {
                        baseline += 1;
                    }
                }*/
                return baseline;
            case JTabbedPane.LEFT:
            case JTabbedPane.RIGHT:
                baseline += insets.top + tabAreaInsets.top;
                /*
                if (isWindows()) {
                    baseline += (maxHeight % 2);
                }*/
                return baseline;
            }
        }
        return -1;
    }

    private int getSliderBaseline(JSlider slider, int height) {
        if (slider.getPaintLabels()) {
            FontMetrics metrics = slider.getFontMetrics(slider.getFont());
            Insets insets = slider.getInsets();
            Insets focusInsets = (Insets)UIManager.get("Slider.focusInsets");
	    if (slider.getOrientation() == JSlider.HORIZONTAL) {
                int tickLength = 8;
                int contentHeight = height - insets.top - insets.bottom -
                    focusInsets.top - focusInsets.bottom;
                int thumbHeight = 20;
                /*
                if (isMetal()) {
                    tickLength = ((Integer)UIManager.get(
                                      "Slider.majorTickLength")).intValue() + 5;
                    thumbHeight = UIManager.getIcon(
                        "Slider.horizontalThumbIcon" ).getIconHeight();
                }
                else if (isWindows() && isXP()) {
                    // PENDING: this is not correct, this should come from
                    // the skin (in >= 1.5), but short of reflection
                    // hacks we don't have access to the real value.
                    thumbHeight++;
                }*/
                int centerSpacing = thumbHeight;
                if (slider.getPaintTicks()) {
                    // centerSpacing += getTickLength();
                    centerSpacing += tickLength;
                }
                // Assume uniform labels.
                centerSpacing += metrics.getAscent() + metrics.getDescent();
                int trackY = insets.top + focusInsets.top +
                    (contentHeight - centerSpacing - 1) / 2;
                int trackHeight = thumbHeight;
                int tickY = trackY + trackHeight;
                int tickHeight = tickLength;
                if (!slider.getPaintTicks()) {
                    tickHeight = 0;
                }
                int labelY = tickY + tickHeight;
                return labelY + metrics.getAscent();
            }
            else { // vertical
                boolean inverted = slider.getInverted();
                Integer value = inverted ? getMinSliderValue(slider) :
                                           getMaxSliderValue(slider);
                if (value != null) {
                    int thumbHeight = 11;
                    /*
                    if (isMetal()) {
                        thumbHeight = UIManager.getIcon(
                            "Slider.verticalThumbIcon").getIconHeight();
                    }*/
                    int trackBuffer = Math.max(metrics.getHeight() / 2,
                                               thumbHeight / 2);
                    int contentY = focusInsets.top + insets.top;
                    int trackY = contentY + trackBuffer;
                    int trackHeight = height - focusInsets.top -
                        focusInsets.bottom - insets.top - insets.bottom -
                        trackBuffer - trackBuffer;
                    int maxValue = getMaxSliderValue(slider).intValue();
                    int min = slider.getMinimum();
                    int max = slider.getMaximum();
                    double valueRange = (double)max - (double)min;
                    double pixelsPerValue = (double)trackHeight /
                        (double)valueRange;
                    int trackBottom = trackY + (trackHeight - 1);
                    int yPosition;

                    if (!inverted) {
                        yPosition = trackY;
                        yPosition += Math.round(pixelsPerValue *
                                            ((double)max - value.intValue()));
                    }
                    else {
                        yPosition = trackY;
                        yPosition += Math.round(pixelsPerValue *
                                           ((double)value.intValue() - min) );
                    }
                    yPosition = Math.max(trackY, yPosition);
                    yPosition = Math.min(trackBottom, yPosition);
                    return yPosition - metrics.getHeight() / 2 +
                        metrics.getAscent();
                }
            }
        }
        return -1;
    }

    private Integer getMaxSliderValue(JSlider slider) {
        Dictionary dictionary = slider.getLabelTable();
        if (dictionary != null) {
            Enumeration keys = dictionary.keys();
            int max = slider.getMinimum() - 1;
            while (keys.hasMoreElements()) {
                max = Math.max(max, ((Integer)keys.nextElement()).intValue());
            }
            if (max == slider.getMinimum() - 1) {
                return null;
            }
            return new Integer(max);
        }
        return null;
    }

    private Integer getMinSliderValue(JSlider slider) {
        Dictionary dictionary = slider.getLabelTable();
        if (dictionary != null) {
            Enumeration keys = dictionary.keys();
            int min = slider.getMaximum() + 1;
            while (keys.hasMoreElements()) {
                min = Math.min(min, ((Integer)keys.nextElement()).intValue());
            }
            if (min == slider.getMaximum() + 1) {
                return null;
            }
            return new Integer(min);
        }
        return null;
    }

    private int getProgressBarBaseline(JProgressBar pb, int height) {
        if (pb.isStringPainted() &&
                pb.getOrientation() == JProgressBar.HORIZONTAL) {
            FontMetrics metrics = pb.getFontMetrics(pb.getFont());
            Insets insets = pb.getInsets();
            int y = insets.top;
            /*
            if (isWindows() && isXP()) {
                if (pb.isIndeterminate()) {
                    y = -1;
                    height--;
                }
                else {
                    y = 0;
                    height -= 3;
                }
            }
            else if (isGTK()) {
                return (height - metrics.getAscent() - 
                        metrics.getDescent()) / 2 + metrics.getAscent();
            }
            else {*/
                height -= insets.top + insets.bottom;
            //}
            return y + (height + metrics.getAscent() -
                        metrics.getLeading() -
                        metrics.getDescent()) / 2;
        }
        return -1;
    }

    private int getTreeBaseline(JTree tree, int height) {
        int rowHeight = tree.getRowHeight();
        if (TREE_LABEL == null) {
            TREE_LABEL = new JLabel("X");
            TREE_LABEL.setIcon(UIManager.getIcon("Tree.closedIcon"));
        }
        JLabel label = TREE_LABEL;
        label.setFont(tree.getFont());
        if (rowHeight <= 0) {
            rowHeight = label.getPreferredSize().height;
        }
        return getLabelBaseline(label, rowHeight) + tree.getInsets().top;
    }

    private int getTableBaseline(JTable table, int height) {
        if (TABLE_LABEL == null) {
            TABLE_LABEL = new JLabel("");
            TABLE_LABEL.setBorder(new EmptyBorder(1, 1, 1, 1));
        }
        JLabel label = TABLE_LABEL;
        label.setFont(table.getFont());
        int rowMargin = table.getRowMargin();
        int baseline = getLabelBaseline(label, table.getRowHeight() -
                                        rowMargin);
        return baseline += rowMargin / 2;
    }

    private int getTextAreaBaseline(JTextArea text, int height) {
        Insets insets = text.getInsets();
        FontMetrics fm = text.getFontMetrics(text.getFont());
        return insets.top + fm.getAscent();
    }
    
    private int getListBaseline(JList list, int height) {
        int rowHeight = list.getFixedCellHeight();
        if (LIST_LABEL == null) {
            LIST_LABEL = new JLabel("X");
            LIST_LABEL.setBorder(new EmptyBorder(1, 1, 1, 1));
        }
        JLabel label = LIST_LABEL;
        label.setFont(list.getFont());
        // JList actually has much more complex behavior here.
        // If rowHeight != -1 the rowHeight is either the max of all cell
        // heights (layout orientation != VERTICAL), or is variable depending
        // upon the cell.  We assume a default size.
        // We could theoretically query the real renderer, but that would
        // not work for an empty model and the results may vary with 
        // the content.
        if (rowHeight == -1) {
            rowHeight = label.getPreferredSize().height;
        }
        return getLabelBaseline(label, rowHeight) + list.getInsets().top;
    }

    private int getScrollPaneBaseline(JScrollPane sp, int height) {
        Component view = sp.getViewport().getView();
        if (view instanceof JComponent) {
            int baseline = getBaseline((JComponent)view);
            if (baseline > 0) {
                return baseline + sp.getViewport().getY();
            }
        }
        return -1;
    }

    private int getPanelBaseline(JPanel panel, int height) {
        Border border = panel.getBorder();
        if (border instanceof TitledBorder) {
            TitledBorder titledBorder = (TitledBorder)border;
            if (titledBorder.getTitle() != null &&
                      !"".equals(titledBorder.getTitle())) {
                Font font = titledBorder.getTitleFont();
                if (font == null) {
                    font = panel.getFont();
                    if (font == null) {
                        font = new Font("Dialog", Font.PLAIN, 12);
                    }
                }
                Border border2 = titledBorder.getBorder();
                Insets borderInsets;
                if (border2 != null) {
                    borderInsets = border2.getBorderInsets(panel);
                }
                else {
                    borderInsets = EMPTY_INSETS;
                }
                FontMetrics fm = panel.getFontMetrics(font);
                int fontHeight = fm.getHeight();
                int descent = fm.getDescent();
                int ascent = fm.getAscent();
                int y = EDGE_SPACING;
                int h = height - EDGE_SPACING * 2;
                int diff;
                switch (((TitledBorder)border).getTitlePosition()) {
                case TitledBorder.ABOVE_TOP:
                    diff = ascent + descent + (Math.max(EDGE_SPACING,
                                    TEXT_SPACING*2) - EDGE_SPACING);
                    return y + diff - (descent + TEXT_SPACING);
                case TitledBorder.TOP:
                case TitledBorder.DEFAULT_POSITION:
                    diff = Math.max(0, ((ascent/2) + TEXT_SPACING) -
                                    EDGE_SPACING);
                    return (y + diff - descent) +
                           (borderInsets.top + ascent + descent)/2;
                case TitledBorder.BELOW_TOP:
                    return y + borderInsets.top + ascent + TEXT_SPACING;
                case TitledBorder.ABOVE_BOTTOM:
                    return (y + h) -
                        (borderInsets.bottom + descent + TEXT_SPACING);
                case TitledBorder.BOTTOM:
                    h -= fontHeight / 2;
                    return ((y + h) - descent) +
                           ((ascent + descent) - borderInsets.bottom)/2;
                case TitledBorder.BELOW_BOTTOM:
                    h -= fontHeight;
                    return y + h + ascent + TEXT_SPACING;
                }
            }
        }
        return -1;
    }

    private int getSpinnerBaseline(JSpinner spinner, int height) {
        JComponent editor = spinner.getEditor();
        if (editor instanceof JSpinner.DefaultEditor) {
            JSpinner.DefaultEditor defaultEditor = (JSpinner.DefaultEditor)
                                          editor;
            JTextField tf = defaultEditor.getTextField();
            Insets spinnerInsets = spinner.getInsets();
            Insets editorInsets = defaultEditor.getInsets();
            int offset = spinnerInsets.top + editorInsets.top;
            height -= (offset + spinnerInsets.bottom + editorInsets.bottom);
            if (height <= 0) {
                return -1;
            }
            return offset + getSingleLineTextBaseline(tf, height);
        }
        Insets insets = spinner.getInsets();
        FontMetrics fm = spinner.getFontMetrics(spinner.getFont());
        return insets.top + fm.getAscent();
    }

    private int getLabelBaseline(JLabel label, int height) {
        Icon icon = (label.isEnabled()) ? label.getIcon() :
                           label.getDisabledIcon();
        FontMetrics fm = label.getFontMetrics(label.getFont());

        resetRects(label, height);

        SwingUtilities.layoutCompoundLabel(label, fm,
            "a", icon, label.getVerticalAlignment(),
            label.getHorizontalAlignment(), label.getVerticalTextPosition(),
            label.getHorizontalTextPosition(), viewRect, iconRect, textRect,
            label.getIconTextGap());

        return textRect.y + fm.getAscent();
    }

    private int getComboBoxBaseline(JComboBox combobox, int height) {
        Insets insets = combobox.getInsets();
        int y = insets.top;
        height -= (insets.top + insets.bottom);
        if (combobox.isEditable()) {
            ComboBoxEditor editor = combobox.getEditor();
            if (editor != null && (editor.getEditorComponent() instanceof
                                   JTextField)) {
                JTextField tf = (JTextField)editor.getEditorComponent();
                return y + getSingleLineTextBaseline(tf, height);
            }
        }
        y -= 1;
        // Use the renderer to calculate baseline
        ListCellRenderer renderer = combobox.getRenderer();
        if (renderer instanceof JLabel) {
            return y + getLabelBaseline((JLabel)renderer, height);
        }
        // Renderer isn't a label, use metrics directly.
        FontMetrics fm = combobox.getFontMetrics(combobox.getFont());
        return y + fm.getAscent();
    }

    /**
     * Returns the baseline for single line text components, like
     * <code>JTextField</code>.
     */
    private int getSingleLineTextBaseline(JTextComponent textComponent,
                                                 int h) {
        View rootView = textComponent.getUI().getRootView(textComponent);
        if (rootView.getViewCount() > 0) {
            Insets insets = textComponent.getInsets();
            int height = h - insets.top - insets.bottom;
            int y = insets.top;
            View fieldView = rootView.getView(0);
	    int vspan = (int)fieldView.getPreferredSpan(View.Y_AXIS);
	    if (height != vspan) {
		int slop = height - vspan;
		y += slop / 2;
	    }
            FontMetrics fm = textComponent.getFontMetrics(
                                 textComponent.getFont());
            y += fm.getAscent();
            return y;
        }
        return -1;
    }

    /**
     * Returns the baseline for buttons.
     */
    private int getCheckBoxBaseline(AbstractButton button, int height) {
        FontMetrics fm = button.getFontMetrics(button.getFont());

        resetRects(button, height);

        // NOTE: that we use "a" here to make sure we get a valid value, if
        // we were to pass in an empty string or null we would not get
        // back the right thing.
        SwingUtilities.layoutCompoundLabel(
            button, fm, "a", button.getIcon(), 
            button.getVerticalAlignment(), button.getHorizontalAlignment(),
            button.getVerticalTextPosition(),
            button.getHorizontalTextPosition(),
            viewRect, iconRect, textRect, 
	    button.getText() == null ? 0 : button.getIconTextGap());

        return textRect.y + fm.getAscent();
        }
    /**
     * Returns the baseline for buttons.
     */
    private int getButtonBaseline(AbstractButton button, int height) {
        FontMetrics fm = button.getFontMetrics(button.getFont());

        resetRects(button, height);

        // NOTE: that we use "a" here to make sure we get a valid value, if
        // we were to pass in an empty string or null we would not get
        // back the right thing.
        SwingUtilities.layoutCompoundLabel(
            button, fm, "a", button.getIcon(), 
            button.getVerticalAlignment(), button.getHorizontalAlignment(),
            button.getVerticalTextPosition(),
            button.getHorizontalTextPosition(),
            viewRect, iconRect, textRect, 
	    button.getText() == null ? 0 : button.getIconTextGap());

        return textRect.y + fm.getAscent() + 1;
    }

    private void resetRects(JComponent c, int height) {
        Insets insets = c.getInsets();
        viewRect.x = insets.left;
        viewRect.y = insets.top;
        viewRect.width = c.getWidth() - (insets.right + viewRect.x);
        viewRect.height = height - (insets.bottom + viewRect.y);
        textRect.x = textRect.y = textRect.width = textRect.height = 0;
        iconRect.x = iconRect.y = iconRect.width = iconRect.height = 0;
    }
    
    private static boolean isMac() {
        return UIManager.getLookAndFeel().getID() == "Mac";
    }

    private static boolean isAqua() {
        return UIManager.getLookAndFeel().getID() == "Aqua";
    }
}
