/*
 * Copyright (c) 2007, 2011, Oracle and/or its affiliates. All rights reserved.
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
 *
 * This code is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License version 2 only, as
 * published by the Free Software Foundation.  Oracle designates this
 * particular file as subject to the "Classpath" exception as provided
 * by Oracle in the LICENSE file that accompanied this code.
 *
 * This code is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
 * version 2 for more details (a copy is included in the LICENSE file that
 * accompanied this code).
 *
 * You should have received a copy of the GNU General Public License version
 * 2 along with this work; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
 *
 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
 * or visit www.oracle.com if you need additional information or have any
 * questions.
 */

package com.sun.tools.visualvm.charts.xy;

import com.sun.tools.visualvm.charts.swing.RotateLabelUI;
import com.sun.tools.visualvm.uisupport.HTMLTextArea;
import com.sun.tools.visualvm.uisupport.TransparentToolBar;
import com.sun.tools.visualvm.uisupport.UISupport;
import com.sun.tools.visualvm.uisupport.VerticalLayout;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ComponentAdapter;
import java.awt.event.ComponentEvent;
import java.text.Format;
import java.text.MessageFormat;
import java.text.NumberFormat;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.swing.Action;
import javax.swing.BorderFactory;
import javax.swing.Icon;
import javax.swing.JCheckBox;
import javax.swing.JComponent;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JScrollBar;
import javax.swing.SwingConstants;
import javax.swing.plaf.LabelUI;
import org.netbeans.lib.profiler.charts.ChartItem;
import org.netbeans.lib.profiler.charts.ChartItemChange;
import org.netbeans.lib.profiler.charts.ChartSelectionModel;
import org.netbeans.lib.profiler.charts.ItemPainter;
import org.netbeans.lib.profiler.charts.ItemsModel;
import org.netbeans.lib.profiler.charts.axis.AxisComponent;
import org.netbeans.lib.profiler.charts.axis.AxisMarksPainter;
import org.netbeans.lib.profiler.charts.axis.BytesMarksPainter;
import org.netbeans.lib.profiler.charts.axis.TimeAxisUtils;
import org.netbeans.lib.profiler.charts.axis.TimeMarksPainter;
import org.netbeans.lib.profiler.charts.axis.TimelineMarksComputer;
import org.netbeans.lib.profiler.charts.swing.CrossBorderLayout;
import org.netbeans.lib.profiler.charts.swing.LongRect;
import org.netbeans.lib.profiler.charts.xy.BytesXYItemMarksComputer;
import org.netbeans.lib.profiler.charts.xy.DecimalXYItemMarksComputer;
import org.netbeans.lib.profiler.charts.xy.XYItemPainter;
import org.netbeans.lib.profiler.charts.xy.synchronous.SynchronousXYItem;
import org.netbeans.lib.profiler.charts.xy.synchronous.SynchronousXYItemsModel;
import org.openide.util.NbBundle;

/**
 *
 * @author Jiri Sedlacek
 */
public class SimpleXYChartUtils {
    
    // --- Public chart types constants ----------------------------------------

    public static final int TYPE_DECIMAL = 0;
    public static final int TYPE_BYTES = 1;
    public static final int TYPE_PERCENT = 2;


    // --- Private constants ---------------------------------------------------

    private static final NumberFormat DECIMAL_FORMATTER;
    private static final NumberFormat PERCENT_FORMATTER;

    private static final int DEFAULT_BUFFER_STEP;

    private static final Color AXIS_FONT_COLOR;
    private static final Color BACKGROUND_COLOR;

    private static final int VALUES_SPACING;
    private static final int TIMELINE_SPACING;

    private static       int LEGEND_HEIGHT;
    private static final int DETAILS_HEIGHT_THRESHOLD;

    private static final Map<String, Format> FORMATS;
    
    // --- Static initializer --------------------------------------------------

    static {
        DECIMAL_FORMATTER = NumberFormat.getNumberInstance();
        DECIMAL_FORMATTER.setGroupingUsed(true);
        DECIMAL_FORMATTER.setMaximumFractionDigits(2);

        PERCENT_FORMATTER = NumberFormat.getPercentInstance();
        PERCENT_FORMATTER.setMinimumFractionDigits(1);
        PERCENT_FORMATTER.setMaximumIntegerDigits(3);

        DEFAULT_BUFFER_STEP = 50;

        AXIS_FONT_COLOR = new Color(90, 90, 90);
        BACKGROUND_COLOR = UISupport.getDefaultBackground();

        VALUES_SPACING = Math.max(new TimeMarksPainter().getFont().getSize(), 15) + 10;
        TIMELINE_SPACING = 80;

        LEGEND_HEIGHT = -1;
        DETAILS_HEIGHT_THRESHOLD = 180;

        FORMATS = new HashMap();
    }
    
    
    // --- Public utils --------------------------------------------------------

    public static XYStorage createStorage(int valuesBuffer) {
        return new XYStorage(valuesBuffer, DEFAULT_BUFFER_STEP);
    }

    public static SynchronousXYItemsModel createItemsModel(XYStorage storage,
                                                           String[] itemNames,
                                                           long minValue,
                                                           long maxValue) {

        SynchronousXYItem[] items = new SynchronousXYItem[itemNames.length];
        for (int i = 0; i < items.length; i++)
            items[i] = storage.addItem(itemNames[i], minValue, maxValue);

        return new SynchronousXYItemsModel(storage, items);
    }

    public static XYPaintersModel createPaintersModel(float[] lineWidths,
                                                      Color[] lineColors,
                                                      Color[] fillColors1,
                                                      Color[] fillColors2,
                                                      ItemsModel itemsModel) {

        ChartItem[] items = new ChartItem[itemsModel.getItemsCount()];
        for (int i = 0; i < items.length; i++)
            items[i] = itemsModel.getItem(i);

        ItemPainter[] painters = new ItemPainter[items.length];
        for (int i = 0; i < painters.length; i++)
            painters[i] = XYPainter.absolutePainter(lineWidths  == null ? null : lineWidths[i],
                                                    lineColors  == null ? null : lineColors[i],
                                                    fillColors1 == null ? null : fillColors1[i],
                                                    fillColors2 == null ? null : fillColors2[i]);
        return new XYPaintersModel(items, painters);
    }

    public static JComponent createChartUI(String chartTitle, String xAxisDescription,
                                           String yAxisDescription, int chartType,
                                           Color[] itemColors, long initialYMargin,
                                           boolean hideItems, boolean legendVisible,
                                           boolean supportsZooming, double chartFactor,
                                           NumberFormat customFormat, XYStorage storage,
                                           SynchronousXYItemsModel itemsModel,
                                           XYPaintersModel paintersModel) {

        // Chart
        final boolean hasAxisLabel = xAxisDescription != null || yAxisDescription != null;
        
        final XYStorage _storage = storage;
        SimpleXYChart chart = new SimpleXYChart(itemsModel, paintersModel) {
            protected void itemsChanged(List<ChartItemChange> itemChanges) {
                if (_storage.isFull()) updateChart(); // full repaint to handle removed items
                else super.itemsChanged(itemChanges);
            }
            public void setBounds(int x, int y, int w, int h) {
                super.setBounds(x, y, w, h);
                
                JScrollBar scroller = (JScrollBar)getClientProperty("scroller"); // NOI18N
                if (scroller != null) {
                    int xpos = getX() - 1;
                    if (hasAxisLabel) xpos += getParent().getX();
                    scroller.setBounds(xpos, 0, getWidth() + 1, scroller.getHeight());
                }
            }
        };

        chart.setFitsHeight(true);
        chart.setFitsWidth(true);
        chart.setBottomBased(true);
        chart.setViewInsets(new Insets(10, 0, 0, 0));
        chart.setInitialDataBounds(new LongRect(System.currentTimeMillis(), 0,
                                       2500, initialYMargin));
        
        chart.addPreDecorator(new XYBackground());
        
        // Horizontal axis
        TimelineMarksComputer hComputer = new TimelineMarksComputer(storage,
                         chart.getChartContext(), SwingConstants.HORIZONTAL) {
            protected int getMinMarksDistance() { return TIMELINE_SPACING; }
        };
        AxisComponent hAxis =
                new XYAxisComponent(chart, hComputer, customizeMarksPainter(
                                    new TimeMarksPainter()), SwingConstants.SOUTH,
                                    AxisComponent.MESH_FOREGROUND);

        // Vertical axis
        AxisComponent vAxis = null;
        if (chartType == TYPE_PERCENT) {
            SynchronousXYItem item = itemsModel.getItem(0);
            XYItemPainter painter = (XYItemPainter)paintersModel.getPainter(item);
            DecimalXYItemMarksComputer vComputer = new DecimalXYItemMarksComputer(
                         item, painter, chart.getChartContext(),
                         SwingConstants.VERTICAL) {
                protected int getMinMarksDistance() { return VALUES_SPACING; }
            };
            vAxis = new XYAxisComponent(chart, vComputer, customizeMarksPainter(
                         new XYPercentMarksPainter(0, 100, chartFactor)),
                         SwingConstants.WEST, AxisComponent.MESH_FOREGROUND);
        } else if (chartType == TYPE_BYTES) {
            SynchronousXYItem item = itemsModel.getItem(0);
            XYItemPainter painter = (XYItemPainter)paintersModel.getPainter(item);
            BytesXYItemMarksComputer vComputer = new BytesXYItemMarksComputer(
                         item, painter, chart.getChartContext(),
                         SwingConstants.VERTICAL) {
                protected int getMinMarksDistance() { return VALUES_SPACING; }
            };
            vAxis = new XYAxisComponent(chart, vComputer, customizeMarksPainter(
                         new BytesMarksPainter()), SwingConstants.WEST,
                         AxisComponent.MESH_FOREGROUND);
        } else {
            SynchronousXYItem item = itemsModel.getItem(0);
            XYItemPainter painter = (XYItemPainter)paintersModel.getPainter(item);
            final DecimalXYItemMarksComputer vComputer = new DecimalXYItemMarksComputer(
                         item, painter, chart.getChartContext(),
                         SwingConstants.VERTICAL) {
                protected int getMinMarksDistance() { return VALUES_SPACING; }
            };
            NumberFormat format = customFormat != null ? customFormat : DECIMAL_FORMATTER;
            vAxis = new XYAxisComponent(chart, vComputer, customizeMarksPainter(
                         new XYDecimalMarksPainter(chartFactor, format)),
                         SwingConstants.WEST, AxisComponent.MESH_FOREGROUND);
        }

        // Tooltip support
        XYTooltipPainter tooltipPainter = new XYTooltipPainter(createTooltipModel(
                                                               chartType,
                                                               itemColors,
                                                               chartFactor,
                                                               customFormat,
                                                               storage,
                                                               itemsModel));
        chart.addOverlayComponent(new XYTooltipOverlay(chart, tooltipPainter));
        chart.getSelectionModel().setHoverMode(ChartSelectionModel.HOVER_EACH_NEAREST);

        // Hovering support
        XYSelectionOverlay selectionOverlay = new XYSelectionOverlay();
        chart.addOverlayComponent(selectionOverlay);
        selectionOverlay.registerChart(chart);
        chart.getSelectionModel().setMoveMode(ChartSelectionModel.SELECTION_LINE_V);
        
        // Chart panel
        JPanel chartPanel = new JPanel(new CrossBorderLayout());
        chartPanel.add(chart, new Integer[] { SwingConstants.CENTER });
        chartPanel.add(hAxis, new Integer[] { SwingConstants.SOUTH,
                                              SwingConstants.SOUTH_WEST });
        chartPanel.add(vAxis, new Integer[] { SwingConstants.WEST,
                                              SwingConstants.SOUTH_WEST });

        // Chart container
        JPanel chartContainer = hasAxisLabel ? new JPanel(new BorderLayout()) :
                                               chartPanel;

        if (hasAxisLabel) {
            chartPanel.setOpaque(false);

            if (xAxisDescription != null)
                chartContainer.add(createXAxisLabel(xAxisDescription), BorderLayout.SOUTH);
            if (yAxisDescription != null)
                chartContainer.add(createYAxisLabel(yAxisDescription), BorderLayout.WEST);

            chartContainer.add(chartPanel, BorderLayout.CENTER);
        }

        chartContainer.setBackground(BACKGROUND_COLOR);

        // Caption panel
        JPanel captionPanel = new JPanel(new BorderLayout());
        captionPanel.setBackground(BACKGROUND_COLOR);
        if (chartTitle != null) captionPanel.add(createTitleLabel(chartTitle),
                                                 BorderLayout.NORTH);
        
        // Side panel
        JPanel sidePanel = new JPanel(new VerticalLayout(false));
        sidePanel.setOpaque(false);
        sidePanel.setBorder(BorderFactory.createEmptyBorder(10, 5, 0, 10));

        // Legend panel
        JComponent legendPanel = createLegendPanel(itemColors, hideItems,
                                                   itemsModel, paintersModel);
        legendPanel.setVisible(legendVisible);
        
        // Scroller panel
        JPanel scrollerPanel = new JPanel(null) {
            public Dimension getPreferredSize() {
                Component c = getComponentCount() > 0 ? getComponent(0) : null;
                if (c != null && c.isVisible()) {
                    Dimension size = c.getSize();
                    size.width += c.getX();
                    return size;
                } else {
                    return new Dimension();
                }
            }
        };
        scrollerPanel.setOpaque(false);
        
        // Bottom panel
        JPanel bottomPanel = new JPanel(new BorderLayout());
        bottomPanel.setOpaque(false);
        bottomPanel.add(scrollerPanel, BorderLayout.NORTH);
        bottomPanel.add(legendPanel, BorderLayout.SOUTH);
        
        // Chart view
        JPanel chartView = new JPanel(new BorderLayout());
        chartView.setBackground(BACKGROUND_COLOR);
        chartView.add(captionPanel, BorderLayout.NORTH);
        chartView.add(chartContainer, BorderLayout.CENTER);
        chartView.add(sidePanel, BorderLayout.EAST);
        chartView.add(bottomPanel, BorderLayout.SOUTH);
        
        chartView.putClientProperty("chart", chart); // NOI18N
        chartView.putClientProperty("sidePanel", sidePanel); // NOI18N
        chartView.putClientProperty("legendPanel", legendPanel); // NOI18N
        chartView.putClientProperty("scrollerPanel", scrollerPanel); // NOI18N
        
        if (supportsZooming) setZoomingEnabled(chartView, supportsZooming);

        return chartView;
    }

    public static DetailsHandle createDetailsArea(final String[] detailsItems,
                                                  JComponent chartContainer) {        
        final HTMLTextArea detailsArea = new HTMLTextArea();
        detailsArea.setBorder(BorderFactory.createEmptyBorder(8, 10, 0, 10));
        detailsArea.setText(createDetailsString(detailsItems, null));

        BorderLayout containerLayout = (BorderLayout)chartContainer.getLayout();
        JComponent containerNorth = (JComponent)containerLayout.
                                    getLayoutComponent(BorderLayout.NORTH);
        containerNorth.add(detailsArea, BorderLayout.CENTER);

        final JComponent containerCenter = (JComponent)containerLayout.
                                           getLayoutComponent(BorderLayout.CENTER);
        containerCenter.setBorder(BorderFactory.createEmptyBorder(6, 10, 0, 0));

        chartContainer.addComponentListener(new ComponentAdapter() {
            public void componentResized(ComponentEvent e) {
                boolean visible = e.getComponent().getHeight() > DETAILS_HEIGHT_THRESHOLD;
                detailsArea.setVisible(visible);
                containerCenter.setBorder(BorderFactory.createEmptyBorder(
                                          visible ? 6 : 10, 10, 0, 0));
            }
        });

        return new DetailsHandle() {
            public void updateDetails(String[] details) {
                try {
                    int selStart = detailsArea.getSelectionStart();
                    int selEnd   = detailsArea.getSelectionEnd();
                    detailsArea.setText(createDetailsString(detailsItems, details));
                    detailsArea.select(selStart, selEnd);
                } catch (Exception e) {}
            }
        };
    }

    public static JComponent createLegendPanel(Color[] itemColors, boolean hideItems,
                                               SynchronousXYItemsModel itemsModel,
                                               final XYPaintersModel paintersModel) {
        JPanel legendPanel = new JPanel(new FlowLayout(FlowLayout.LEADING, hideItems ? 5 : 10, 0));
        legendPanel.setBackground(BACKGROUND_COLOR);
        
        for (int i = 0; i < itemColors.length; i++) {
            final SynchronousXYItem item = itemsModel.getItem(i);
            ColorIcon icon = new ColorIcon(itemColors[i], Color.DARK_GRAY, 10, 10);
            JComponent legendItem = null;

            if (hideItems) {
                final XYPainter painter = (XYPainter)paintersModel.getPainter(item);
                legendItem = new IconCheckBox(item.getName(), icon, painter.isPainting()) {
                    public Dimension getPreferredSize() {
                        Dimension ps = super.getPreferredSize();
                        ps.height = getLegendHeight();
                        return ps;
                    }
                    protected void fireActionPerformed(ActionEvent e) {
                        painter.setPainting(isSelected());
                        paintersModel.painterChanged(painter);
                    }
                };
            } else {
                legendItem = new JLabel(item.getName(), icon, JLabel.HORIZONTAL) {
                    public Dimension getPreferredSize() {
                        Dimension ps = super.getPreferredSize();
                        ps.height = getLegendHeight();
                        return ps;
                    }
                };
            }
            
            legendItem.setOpaque(false);
            legendPanel.add(legendItem);

        }

        JPanel legendContainer = new JPanel(new FlowLayout(FlowLayout.TRAILING, 0, 0));
        legendContainer.setOpaque(true);
        legendContainer.setBackground(BACKGROUND_COLOR);
        legendContainer.setBorder(BorderFactory.createMatteBorder(
                                  0, 10, 0, 0, BACKGROUND_COLOR));
        legendContainer.add(legendPanel);

        return legendContainer;
    }
    
    public static void setZoomingEnabled(JComponent chartUI, boolean enabled) {
        SimpleXYChart chart = (SimpleXYChart)chartUI.getClientProperty("chart"); // NOI18N
        
        if (chart.isZoomingEnabled() == enabled) return;
        else chart.setZoomingEnabled(enabled);
        
        JPanel sidePanel = (JPanel)chartUI.getClientProperty("sidePanel"); // NOI18N
        JPanel scrollerPanel = (JPanel)chartUI.getClientProperty("scrollerPanel"); // NOI18N
        
        if (enabled) {
            TransparentToolBar toolbar = new TransparentToolBar(false);
            for (Action action : chart.getActions()) toolbar.addItem(action);
            sidePanel.add(toolbar);
            
            JScrollBar scroller = chart.getScroller();
            scroller.setSize(scroller.getPreferredSize());
            scrollerPanel.add(scroller);
            chart.putClientProperty("scroller", scroller); // NOI18N
        } else {
            sidePanel.removeAll();
            scrollerPanel.removeAll();
            chart.putClientProperty("scroller", null); // NOI18N
        }
        
        sidePanel.setVisible(enabled);
        
        chartUI.doLayout();
        chartUI.repaint();
    }
    
    public static void setLegendVisible(JComponent chartUI, boolean visible) {
        JPanel legendPanel = (JPanel)chartUI.getClientProperty("legendPanel"); // NOI18N
        legendPanel.setVisible(visible);
        
        chartUI.doLayout();
        chartUI.repaint();
    }

    public static XYTooltipModel createTooltipModel(final int chartType,
                                                    final Color[] itemColors,
                                                    final double chartFactor,
                                                    final NumberFormat customFormat,
                                                    final XYStorage storage,
                                                    final SynchronousXYItemsModel itemsModel) {

        return new XYTooltipModel() {

            public String getTimeValue(long timestamp) {
                int timestamps = storage.getTimestampsCount();
                if (timestamps == 0) return formatTime(timestamp, timestamp, timestamp);
                else return formatTime(timestamp, storage.getTimestamp(0),
                                       storage.getTimestamp(timestamps - 1));
            }

            public int getRowsCount() {
                return itemsModel.getItemsCount();
            }

            public String getRowName(int index) {
                return itemsModel.getItem(index).getName();
            }

            public Color getRowColor(int index) {
                return itemColors[index];
            }

            public String getRowValue(int index, long itemValue) {
                double value = itemValue * chartFactor;
                switch (chartType) {
                    case TYPE_BYTES  : return formatBytes((long)value);
                    case TYPE_PERCENT: return formatPercent(value);
                    default:           return formatDecimal(value, customFormat);
                }
            }

        };
    }

    public static AxisMarksPainter customizeMarksPainter(AxisMarksPainter.Abstract painter) {
        painter.setForeground(AXIS_FONT_COLOR);
        painter.setFont(smallerFont(painter.getFont()));
        return painter;
    }

    public static String formatDecimal(double value) {
        return DECIMAL_FORMATTER.format(value);
    }
    
    public static String formatDecimal(double value, NumberFormat format) {
        return format != null ? format.format(value) : formatDecimal(value);
    }

    public static String formatBytes(long value) {
        String bytesFormat = NbBundle.getMessage(SimpleXYChartUtils.class,
                                                "SimpleXYChartUtils_BytesFormat"); // NOI18N
        return MessageFormat.format(bytesFormat, new Object[] { formatDecimal(value) });
    }

    public static String formatPercent(double value) {
        return PERCENT_FORMATTER.format(value / 100);
    }

    public static String formatTime(long timestamp, long startTime, long endTime) {
        String formatString = TimeAxisUtils.getFormatString(1000, startTime, endTime);
        return getFormat(formatString).format(new Date(timestamp));
    }

    public static Font smallerFont(Font font) {
        return new Font(font.getName(), font.getStyle(), font.getSize() - 2);
    }

    public static Font boldFont(Font font) {
        return new Font(font.getName(), Font.BOLD, font.getSize());
    }


    // --- Private implementation ----------------------------------------------

    private static String createDetailsString(String[] detailsItems, String[] values) {
        StringBuilder sb = new StringBuilder();
        String itemFormat = NbBundle.getMessage(SimpleXYChartUtils.class,
                                                "SimpleXYChartUtils_DetailsItemFormat"); // NOI18N

        sb.append("<table border='0' cellpadding='0' cellspacing='3' width='100%'>"); // NOI18N
        sb.append("<tr>"); // NOI18N
        int i = 0;
        while (i < detailsItems.length) {
            if (detailsItems.length == 1) sb.append("<td><nobr>"); // NOI18N
            else sb.append("<td width='50%'><nobr>"); // NOI18N
            sb.append(MessageFormat.format(itemFormat, new Object[] { detailsItems[i],
                                           values == null ? "" : values[i] })); // NOI18N
            sb.append("</nobr></td>"); // NOI18N
            if (i % 2 == 1 && i + 1 < detailsItems.length) sb.append("</tr><tr>"); // NOI18N
            i++;
        }
        sb.append("</tr>"); // NOI18N
        sb.append("</table>"); // NOI18N

        return sb.toString();
    }

    private static synchronized Format getFormat(String formatString) {
        Format format = FORMATS.get(formatString);
        if (format == null) {
            format = new SimpleDateFormat(formatString);
            FORMATS.put(formatString, format);
        }
        return format;
    }

    private static synchronized int getLegendHeight() {
        if (LEGEND_HEIGHT == -1)
            LEGEND_HEIGHT = Math.max(new JLabel("X").getPreferredSize().height, // NOI18N
                                     new JCheckBox("X").getPreferredSize().height); // NOI18N
        return LEGEND_HEIGHT;
    }

    private static JLabel createTitleLabel(String text) {
        JLabel label = createRotatedLabel(text, RotateLabelUI.R0);
        label.setBorder(BorderFactory.createEmptyBorder(8, 3, 0, 3));
        label.setFont(boldFont(label.getFont()));
        return label;
    }

    private static JLabel createXAxisLabel(String text) {
        JLabel label = createRotatedLabel(text, RotateLabelUI.R0);
        label.setBorder(BorderFactory.createEmptyBorder(3, 3, 3, 3));
        return label;
    }

    private static JLabel createYAxisLabel(String text) {
        JLabel label = createRotatedLabel(text, RotateLabelUI.R270);
        label.setBorder(BorderFactory.createEmptyBorder(3, 3, 3, 3));
        return label;
    }

    private static JLabel createRotatedLabel(String text, final LabelUI labelUI) {
        return new JLabel(text, SwingConstants.CENTER) {
            public void updateUI() { if (getUI() != labelUI) setUI(labelUI); }
        };
    }


    // --- DetailsHandle -------------------------------------------------------

    public static interface DetailsHandle {
        public void updateDetails(String[] details);
    }


    // --- IconCheckBox --------------------------------------------------------

    private static class IconCheckBox extends JCheckBox {

        private static final int CHECKBOX_OFFSET = getCheckBoxOffset();

        private final JCheckBox renderer;

        public IconCheckBox(String text, Icon icon, boolean selected) {
            renderer = new JCheckBox(text, icon) {
                public boolean hasFocus() {
                    return IconCheckBox.this.hasFocus();
                }
            };
            renderer.setOpaque(false);
            renderer.setBorderPainted(false);
            setSelected(selected);
            setBorderPainted(false);
        }

        protected void paintComponent(Graphics g) {
            super.paintComponent(g);
            g.translate(renderer.getX(), renderer.getY());
            renderer.paint(g);
            g.translate(-renderer.getX(), -renderer.getY());
        }


        public void setBounds(int x, int y, int width, int height) {
            Dimension d = super.getPreferredSize();
            renderer.setBounds(d.width - CHECKBOX_OFFSET, 0,
                               width - d.width + CHECKBOX_OFFSET, height);
            super.setBounds(x, y, width, height);
        }

        public Dimension getPreferredSize() {
            Dimension d = super.getPreferredSize();
            d.width += renderer.getPreferredSize().width - CHECKBOX_OFFSET;
            return d;
        }


        private static int getCheckBoxOffset() {
            if (UISupport.isWindowsLookAndFeel()) return 3;
            else if (UISupport.isNimbusLookAndFeel()) return -3;
            else if (UISupport.isMetalLookAndFeel()) return 3;
            else if (UISupport.isAquaLookAndFeel()) return 6;
            else return 0;
        }

    }

}
