package org.jdesktop.layout;

import java.awt.*;
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.*;

/**
 * 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";
  }
}
