/*
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
 *
 * Copyright 2007-2010 Oracle and/or its affiliates. All rights reserved.
 *
 * Oracle and Java are registered trademarks of Oracle and/or its affiliates.
 * Other names may be trademarks of their respective owners.
 *
 * The contents of this file are subject to the terms of either the GNU
 * General Public License Version 2 only ("GPL") or the Common
 * Development and Distribution License("CDDL") (collectively, 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-gplv2.html
 * or nbbuild/licenses/CDDL-GPL-2-CP. See the License for the
 * specific language governing permissions and limitations under the
 * License.  When distributing the software, include this License Header
 * Notice in each file and include the License file at
 * nbbuild/licenses/CDDL-GPL-2-CP.  Oracle designates this
 * particular file as subject to the "Classpath" exception as provided
 * by Oracle in the GPL Version 2 section of the License file that
 * accompanied this code. If applicable, add the following below the
 * License Header, with the fields enclosed by brackets [] replaced by
 * your own identifying information:
 * "Portions Copyrighted [year] [name of copyright owner]"
 *
 * Contributor(s):
 * 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.
 *
 * If you wish your version of this file to be governed by only the CDDL
 * or only the GPL Version 2, indicate your decision by adding
 * "[Contributor] elects to include this software in this distribution
 * under the [CDDL or GPL Version 2] license." If you do not indicate a
 * single choice of license, a recipient has the option to distribute
 * your version of this file under either the CDDL, the GPL Version 2 or
 * to extend the choice of license to its licensees as provided above.
 * However, if you add GPL Version 2 code and therefore, elected the GPL
 * Version 2 license, then the option applies only if the new code is
 * made subject to such option by the copyright holder.
 */

package org.graalvm.visualvm.lib.charts;

import org.graalvm.visualvm.lib.charts.swing.LongRect;
import org.graalvm.visualvm.lib.charts.swing.Utils;
import org.graalvm.visualvm.lib.charts.canvas.InteractiveCanvasComponent;
import java.awt.Color;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Rectangle;
import java.awt.RenderingHints;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

/**
 *
 * @author Jiri Sedlacek
 */
public class ChartComponent extends InteractiveCanvasComponent {

    private RenderingHints renderingHints;

    private List<ChartDecorator> preDecorators;
    private List<ChartDecorator> postDecorators;

    private ItemsModel itemsModel;
    private PaintersModel paintersModel;

    protected LongRect dataBounds;
    private LongRect initialDataBounds;
    private ChartContext chartContext;

    private ItemsModelListener itemsListener;
    private PaintersModelListener paintersListener;

    private List<ChartOverlay> overlays;

    private ChartSelectionModel selectionModel;
    private SelectionListener selectionListener;

    private List<ChartConfigurationListener> configurationListeners;


    public ChartComponent() {
        initRenderingHints();

        itemsListener = new ItemsModelListener();
        paintersListener = new PaintersModelListener();

        dataBounds = new LongRect();
        initialDataBounds = new LongRect();

        setLayout(null);

        setSelectionModel(new ChartSelectionManager());
    }


    // --- Models --------------------------------------------------------------

    public final void setItemsModel(ItemsModel itemsModel) {
        if (itemsModel == null) throw new IllegalArgumentException("ItemsModel cannot be null");
        if (itemsModel == this.itemsModel) return;

        if (this.itemsModel != null) this.itemsModel.removeItemsListener(itemsListener);

        this.itemsModel = itemsModel;
        this.itemsModel.addItemsListener(itemsListener);

        updateChart();
    }

    public final ItemsModel getItemsModel() {
        return itemsModel;
    }

    public final void setPaintersModel(PaintersModel paintersModel) {
        if (paintersModel == null) throw new IllegalArgumentException("PaintersModel cannot be null");
        if (paintersModel == this.paintersModel) return;

        if (this.paintersModel != null) this.paintersModel.removePaintersListener(paintersListener);

        this.paintersModel = paintersModel;
        this.paintersModel.addPaintersListener(paintersListener);

        updateChart();
    }

    public final PaintersModel getPaintersModel() {
        return paintersModel;
    }

    public final void setSelectionModel(ChartSelectionModel selectionModel) {
        // Cleanup previous model
        if (this.selectionModel != null) {
            this.selectionModel.removeSelectionListener(selectionListener);
            if (this.selectionModel instanceof ChartSelectionManager)
                ((ChartSelectionManager)this.selectionModel).unregisterChart(this);
        }

        // Assign new model
        this.selectionModel = selectionModel;

        // Setup new model
        if (selectionModel != null) {
            if (selectionListener == null) selectionListener = new SelectionListener();
            selectionModel.addSelectionListener(selectionListener);
            if (selectionModel instanceof ChartSelectionManager)
                ((ChartSelectionManager)selectionModel).registerChart(this);
        } else {
            selectionListener = null;
        }
    }

    public final ChartSelectionModel getSelectionModel() {
        return selectionModel;
    }


    // --- Initial data bounds -------------------------------------------------

    public final void setInitialDataBounds(LongRect bounds) {
        if (LongRect.equals(bounds, initialDataBounds)) return;
        LongRect.set(initialDataBounds, bounds);
        if (LongRect.isEmpty(dataBounds)) {
            resizeChart();
            invalidateImage();
            repaintDirty();
        }
    }

    public final LongRect getInitialDataBounds() {
        return initialDataBounds;
    }

    // --- Customizable RenderingHints -----------------------------------------

    public final void setRenderingHints(RenderingHints renderingHints) {
        this.renderingHints = Utils.checkedRenderingHints(renderingHints);
    }

    public final RenderingHints getRenderingHints() {
        return (RenderingHints)renderingHints.clone();
    }

    private void applyRenderingHints(Graphics2D g) {
        if (renderingHints != null) g.setRenderingHints(renderingHints);
    }

    private void initRenderingHints() {
        RenderingHints hints = new RenderingHints(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
        hints.put(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_SPEED);
        setRenderingHints(hints);
    }


    // --- ChartContext --------------------------------------------------------

    public final ChartContext getChartContext() {
        return getChartContext(null);
    }

    protected ChartContext getChartContext(ChartItem item) {
        if (chartContext == null) chartContext = createChartContext();
        return chartContext;
    }

    protected ChartContext createChartContext() {
        return new Context(this);
    }


    // --- Configuration Listeners ---------------------------------------------

    public final void addConfigurationListener(ChartConfigurationListener listener) {
        if (configurationListeners == null) configurationListeners = new ArrayList();
        configurationListeners.add(listener);
    }

    public final void removeConfigurationListener(ChartConfigurationListener listener) {
        if (configurationListeners == null) return;
        configurationListeners.remove(listener);
    }


    protected final void offsetChanged(long oldOffsetX, long oldOffsetY,
                              long newOffsetX, long newOffsetY) {
        super.offsetChanged(oldOffsetX, oldOffsetY, newOffsetX, newOffsetY);
        fireOffsetChanged(oldOffsetX, oldOffsetY, newOffsetX, newOffsetY);
    }

    protected final void scaleChanged(double oldScaleX, double oldScaleY,
                             double newScaleX, double newScaleY) {
        super.scaleChanged(oldScaleX, oldScaleY, newScaleX, newScaleY);
        fireScaleChanged(oldScaleX, oldScaleY, newScaleX, newScaleY);
    }

    protected final void dataBoundsChanged(long dataOffsetX, long dataOffsetY,
                                  long dataWidth, long dataHeight,
                                  long oldDataOffsetX, long oldDataOffsetY,
                                  long oldDataWidth, long oldDataHeight) {
        super.dataBoundsChanged(dataOffsetX, dataOffsetY, dataWidth, dataHeight,
                                oldDataOffsetX, oldDataOffsetY, oldDataWidth, oldDataHeight);
        fireDataBoundsChanged(dataOffsetX, dataOffsetY, dataWidth, dataHeight,
                              oldDataOffsetX, oldDataOffsetY, oldDataWidth, oldDataHeight);
    }

    protected final void contentsWillBeUpdated(long offsetX, long offsetY,
                               double scaleX, double scaleY,
                               long lastOffsetX, long lastOffsetY,
                               double lastScaleX, double lastScaleY) {
        super.contentsWillBeUpdated(offsetX, offsetY, scaleX, scaleY, lastOffsetX,
                          lastOffsetY, lastScaleX, lastScaleY);
        fireContentsWillBeUpdated(offsetX, offsetY, scaleX, scaleY,
                                  lastOffsetX, lastOffsetY, lastScaleX, lastScaleY);
    }

    protected final void contentsUpdated(long offsetX, long offsetY,
                            double scaleX, double scaleY,
                            long lastOffsetX, long lastOffsetY,
                            double lastScaleX, double lastScaleY,
                            int shiftX, int shiftY) {
        super.contentsUpdated(offsetX, offsetY, scaleX, scaleY, lastOffsetX,
                          lastOffsetY, lastScaleX, lastScaleY, shiftX, shiftY);
        fireContentsUpdated(offsetX, offsetY, scaleX, scaleY, lastOffsetX, lastOffsetY,
                        lastScaleX, lastScaleY, shiftX, shiftY);
    }


    private void fireOffsetChanged(long oldOffsetX, long oldOffsetY,
                                   long newOffsetX, long newOffsetY) {
        if (configurationListeners == null) return;
        for (ChartConfigurationListener listener : configurationListeners)
            listener.offsetChanged(oldOffsetX, oldOffsetY, newOffsetX, newOffsetY);
    }

    private void fireScaleChanged(double oldScaleX, double oldScaleY,
                                  double newScaleX, double newScaleY) {
        if (configurationListeners == null) return;
        for (ChartConfigurationListener listener : configurationListeners)
            listener.scaleChanged(oldScaleX, oldScaleY, newScaleX, newScaleY);
    }

    private void fireDataBoundsChanged(long dataOffsetX, long dataOffsetY,
                                       long dataWidth, long dataHeight,
                                       long oldDataOffsetX, long oldDataOffsetY,
                                       long oldDataWidth, long oldDataHeight) {
        if (configurationListeners == null) return;
        for (ChartConfigurationListener listener : configurationListeners)
            listener.dataBoundsChanged(dataOffsetX, dataOffsetY, dataWidth,
                                       dataHeight, oldDataOffsetX, oldDataOffsetY,
                                       oldDataWidth, oldDataHeight);
    }

    private void fireContentsWillBeUpdated(long offsetX, long offsetY,
                            double scaleX, double scaleY,
                            long lastOffsetX, long lastOffsetY,
                            double lastScaleX, double lastScaleY) {
        if (configurationListeners == null) return;
        for (ChartConfigurationListener listener : configurationListeners)
            listener.contentsWillBeUpdated(offsetX, offsetY, scaleX, scaleY,
                              lastOffsetX, lastOffsetY, lastScaleX, lastScaleY);
    }

    private void fireContentsUpdated(long offsetX, long offsetY,
                            double scaleX, double scaleY,
                            long lastOffsetX, long lastOffsetY,
                            double lastScaleX, double lastScaleY,
                            int shiftX, int shiftY) {
        if (configurationListeners == null) return;
        for (ChartConfigurationListener listener : configurationListeners)
            listener.contentsUpdated(offsetX, offsetY, scaleX, scaleY, lastOffsetX,
                                 lastOffsetY, lastScaleX, lastScaleY, shiftX, shiftY);
    }


    // --- Pre & post painters support -----------------------------------------

    public final void addPreDecorator(ChartDecorator decorator) {
        if (preDecorators == null) preDecorators = new ArrayList(2);
        preDecorators.add(decorator);
    }

    public final void removePreDecorator(ChartDecorator decorator) {
        if (preDecorators != null) preDecorators.remove(decorator);
    }

    final List<ChartDecorator> getPreDecorators() {
        return preDecorators;
    }

    public final void addPostDecorator(ChartDecorator decorator) {
        if (postDecorators == null) postDecorators = new ArrayList(2);
        postDecorators.add(decorator);
    }

    public final void removePostDecorator(ChartDecorator decorator) {
        if (postDecorators != null) postDecorators.remove(decorator);
    }

    final List<ChartDecorator> getPostDecorators() {
        return postDecorators;
    }


    // --- Overlays ------------------------------------------------------------

    public final void addOverlayComponent(ChartOverlay overlay) {
        if (overlays == null) overlays = new ArrayList();

        overlay.setChartContext(getChartContext());

        overlays.add(overlay);
        add(overlay);
    }

    public final void removeOverlayComponent(ChartOverlay overlay) {
        remove(overlay);
        overlays.remove(overlay);

        overlay.setChartContext(null);
    }


    protected void reshaped(Rectangle oldBounds, Rectangle newBounds) {
        super.reshaped(oldBounds, newBounds);

        if (overlays == null) return;

        for (ChartOverlay overlay : overlays)
            overlay.setBounds(0, 0, newBounds.width, newBounds.height);
    }


    // --- Paint implementation ------------------------------------------------

    protected void paintContents(Graphics g, Rectangle invalidArea) {
        Graphics2D g2 = (Graphics2D)g;

        // Set clip
        g2.setClip(invalidArea);

        // Set rendering hints
        applyRenderingHints(g2);

        // Paint background if opaque
        if (isOpaque()) {
            g2.setColor(getBackground());
            g2.fillRect(invalidArea.x, invalidArea.y,
                       invalidArea.width, invalidArea.height);
        }

        // Paint registered prepainters
        if (preDecorators != null)
            for (ChartDecorator decorator : preDecorators)
                decorator.paint(g2, invalidArea, getChartContext());

        // Paint chart items
        if (itemsModel != null && paintersModel != null) {
            int itemsCount = itemsModel.getItemsCount();

            if (itemsCount != 0) {
                boolean sel = selectionModel != null;

                List<ItemSelection> highlightedSelection = sel ? selectionModel.getHighlightedItems() : null;
                List<ItemSelection> selectedSelection = sel ? selectionModel.getSelectedItems() : null;
                List<ItemSelection> filteredHighlighted = sel ? new ArrayList() : Collections.EMPTY_LIST;
                List<ItemSelection> filteredSelected = sel ? new ArrayList() : Collections.EMPTY_LIST;

                for (int i = 0; i < itemsCount; i++) {
                    ChartItem item = itemsModel.getItem(i);
                    ItemPainter painter = paintersModel.getPainter(item);

                    if (sel) {
                        filteredHighlighted.clear();
                        if (painter.supportsHovering(item))
                            filterSelection(highlightedSelection, filteredHighlighted, item);
                        filteredSelected.clear();
                        if (painter.supportsSelecting(item))
                            filterSelection(selectedSelection, filteredSelected, item);
                    }

                    painter.paintItem(item, filteredHighlighted, filteredSelected,
                                      g2, invalidArea, getChartContext(item));
                }
            }
        }

        // Paint registered postpainters
        if (postDecorators != null)
            for (ChartDecorator decorator : postDecorators)
                decorator.paint(g2, invalidArea, getChartContext());
    }


    private static void filterSelection(List<ItemSelection> selection, List<ItemSelection> result, ChartItem filter) {
        if (filter == null) return;
        for (ItemSelection sel : selection)
            if (sel.getItem().equals(filter)) result.add(sel);
    }


    // --- UI tweaks -----------------------------------------------------------

    public void setBackground(Color bg) {
        super.setBackground(Utils.checkedColor(bg));
    }


    // --- Protected implementation ----------------------------------------------

    protected void computeDataBounds() {
        LongRect.clear(dataBounds);
        if (itemsModel == null || paintersModel == null) return;

        int itemsCount = itemsModel.getItemsCount();
        for (int i = 0; i < itemsCount; i++) {
            ChartItem item = itemsModel.getItem(i);
            ItemPainter painter = paintersModel.getPainter(item);
            if (i == 0)
                LongRect.set(dataBounds, painter.getItemBounds(item));
            else
                LongRect.add(dataBounds, painter.getItemBounds(item));
        }
    }

    protected void resizeChart() {
        if (LongRect.isEmpty(dataBounds)) {
            LongRect bounds = new LongRect(dataBounds);
            if (bounds.width == 0) {
                bounds.width = initialDataBounds.width;
                if (bounds.x == 0) bounds.x = initialDataBounds.x;
            }
            if (bounds.height == 0) {
                bounds.height = initialDataBounds.height;
                if (bounds.y == 0) bounds.y = initialDataBounds.y;
            }
            setDataBounds(bounds.x, bounds.y, bounds.width, bounds.height);
        } else {
            setDataBounds(dataBounds.x, dataBounds.y, dataBounds.width, dataBounds.height);
        }
    }

    protected void updateChart() {
        computeDataBounds();
        resizeChart();
        invalidateImage();
        repaintDirty();
    }

    protected void itemsAdded(List<ChartItem> addedItems) {
        // Update chart size
        LongRect oldBounds = new LongRect(dataBounds);
        for (ChartItem item : addedItems) {
            ItemPainter painter = paintersModel.getPainter(item);
            LongRect.add(dataBounds, painter.getItemBounds(item));
        }
        if (!LongRect.equals(oldBounds, dataBounds)) resizeChart();

        // Update chart appearance
        LongRect uiBounds = null;
        for (ChartItem item : addedItems) {
            ItemPainter painter = paintersModel.getPainter(item);
            if (uiBounds == null) uiBounds =
                new LongRect(painter.getItemBounds(item, getChartContext(item)));
            else
                LongRect.add(uiBounds, painter.getItemBounds(item, getChartContext(item)));
        }
        invalidateImage(Utils.checkedRectangle(uiBounds));
        repaintDirty();
    }

    protected void itemsRemoved(List<ChartItem> removedItems) {
        List<ItemPainter> painters = new ArrayList(removedItems.size());

        // Try to resolve painters for all removed items
        for (ChartItem item : removedItems) {
            ItemPainter painter = paintersModel.getPainter(item);
            if (painter == null) {
                painters = null;
                break;
            }
            painters.add(painter);
        }

        if (painters == null) {
            // Some or all painters for removed items not available
            updateChart();
        } else {
            // All painters for removed items available

            // Update chart size
            LongRect oldBounds = new LongRect(dataBounds);
            computeDataBounds();
            if (!LongRect.equals(oldBounds, dataBounds)) resizeChart();

            // Update chart appearance
            LongRect uiBounds = null;
            for (int i = 0; i < removedItems.size(); i++) {
                ChartItem item = removedItems.get(i);
                ItemPainter painter = painters.get(i);
                if (uiBounds == null) uiBounds =
                    new LongRect(painter.getItemBounds(item, getChartContext(item)));
                else
                    LongRect.add(uiBounds, painter.getItemBounds(item, getChartContext(item)));
            }
            invalidateImage(Utils.checkedRectangle(uiBounds));
            repaintDirty();
        }
    }

    protected void itemsChanged(List<ChartItemChange> itemChanges) {
        // Resolve painters for changedItems
        List<ItemPainter> painters = new ArrayList(itemChanges.size());
        for (ChartItemChange change : itemChanges)
            painters.add(paintersModel.getPainter(change.getItem()));

        // Check if items bounds changed
        boolean boundsChange = false;
        for (int i = 0; i < itemChanges.size(); i++) {
            ChartItemChange change = itemChanges.get(i);
            ItemPainter painter = painters.get(i);
            boundsChange = painter.isBoundsChange(change);
            if (boundsChange) break;
        }

        // Update chart size
        if (boundsChange) {
            LongRect oldBounds = new LongRect(dataBounds);
            computeDataBounds();
            if (!LongRect.equals(oldBounds, dataBounds)) resizeChart();
        }

        // Check if items appearance changed
        boolean appearanceChange = false;
        for (int i = 0; i < itemChanges.size(); i++) {
            ChartItemChange change = itemChanges.get(i);
            ItemPainter painter = painters.get(i);
            appearanceChange = painter.isAppearanceChange(change);
            if (appearanceChange) break;
        }

        // Update chart appearance
        if (appearanceChange) {
            LongRect uiBounds = null;
            for (int i = 0; i < itemChanges.size(); i++) {
                ChartItemChange change = itemChanges.get(i);
                ChartItem item = change.getItem();
                ItemPainter painter = paintersModel.getPainter(item);
                if (painter.isAppearanceChange(change)) {
                    if (uiBounds == null) uiBounds =
                        new LongRect(painter.getDirtyBounds(change, getChartContext(item)));
                    else
                        LongRect.add(uiBounds, painter.getDirtyBounds(change, getChartContext(item)));
                }
            }
            invalidateImage(Utils.checkedRectangle(uiBounds));
            repaintDirtyAccel();
        } else {
            repaintDirty();
        }
    }

    protected void paintersChanged() {
        updateChart();
    }

    protected void paintersChanged(List<ItemPainter> changedPainters) {
        Set<ChartItem> changedItems = new HashSet();

        // Update chart size
        LongRect oldBounds = new LongRect(dataBounds);
        computeDataBounds();
        if (!LongRect.equals(oldBounds, dataBounds)) resizeChart();

        // Resolve changed items
        for (int i = 0; i < itemsModel.getItemsCount(); i++) {
            ChartItem item = itemsModel.getItem(i);
            if (changedPainters.contains(paintersModel.getPainter(item)))
                changedItems.add(item);
        }

        // Update chart appearance
        LongRect uiBounds = null;
        for (ChartItem item : changedItems) {
            ItemPainter painter = paintersModel.getPainter(item);
            if (uiBounds == null) uiBounds =
                new LongRect(painter.getItemBounds(item, getChartContext(item)));
            else
                LongRect.add(uiBounds, painter.getItemBounds(item, getChartContext(item)));
        }
        invalidateImage(Utils.checkedRectangle(uiBounds));
        repaintDirty();
    }


    // --- ItemsModel change support -------------------------------------------

    private class ItemsModelListener implements ItemsListener {

        public void itemsAdded(List<ChartItem> addedItems) {
            ChartComponent.this.itemsAdded(addedItems);
        }

        public void itemsRemoved(List<ChartItem> removedItems) {
            ChartComponent.this.itemsRemoved(removedItems);
        }

        public void itemsChanged(List<ChartItemChange> itemChanges) {
            ChartComponent.this.itemsChanged(itemChanges);
        }

    }


    // --- PaintersModel change support ----------------------------------------

    private class PaintersModelListener implements PaintersListener {

        public void paintersChanged() {
            ChartComponent.this.paintersChanged();
        }

        public void paintersChanged(List<ItemPainter> changedPainters) {
            ChartComponent.this.paintersChanged(changedPainters);
        }

    }


    // --- Selection listener --------------------------------------------------

    private class SelectionListener implements ChartSelectionListener {

        public void selectionModeChanged(int newMode, int oldMode) {}

        public void selectionBoundsChanged(Rectangle newBounds, Rectangle oldBounds) {}

        public void highlightedItemsChanged(List<ItemSelection> currentItems,
              List<ItemSelection> addedItems, List<ItemSelection> removedItems) {
            refreshSelection(addedItems, removedItems);
        }

        public void selectedItemsChanged(List<ItemSelection> currentItems,
              List<ItemSelection> addedItems, List<ItemSelection> removedItems) {
            refreshSelection(addedItems, removedItems);
        }


        private void refreshSelection(List<ItemSelection> addedItems, List<ItemSelection> removedItems) {
            // TODO: should be optimized!
            Rectangle dirtyArea = new Rectangle();

            if (!removedItems.isEmpty() && removedItems.get(0) != null)
                for (ItemSelection sel : removedItems) {
                    ChartItem item = sel.getItem();
                    ItemPainter painter = paintersModel.getPainter(item);
                    if (painter.supportsHovering(item)) {
                        if (dirtyArea.isEmpty())
                            dirtyArea.setBounds(Utils.checkedRectangle(
                            painter.getSelectionBounds(sel, getChartContext(item))));
                        else
                            dirtyArea.add(Utils.checkedRectangle(
                            painter.getSelectionBounds(sel, getChartContext(item))));
                    }
                }

            if (!dirtyArea.isEmpty()) {
                invalidateImage(dirtyArea);
                paintImmediately(dirtyArea);
//                repaintDirty();
                dirtyArea = new Rectangle();
            }

            if (!addedItems.isEmpty() && addedItems.get(0) != null)
                for (ItemSelection sel : addedItems) {
                    ChartItem item = sel.getItem();
                    ItemPainter painter = paintersModel.getPainter(item);
                    if (painter.supportsHovering(item)) {
                        if (dirtyArea.isEmpty())
                            dirtyArea.setBounds(Utils.checkedRectangle(
                            painter.getSelectionBounds(sel, getChartContext(item))));
                        else
                            dirtyArea.add(Utils.checkedRectangle(
                            painter.getSelectionBounds(sel, getChartContext(item))));
                    }
                }

            if (!dirtyArea.isEmpty()) {
                invalidateImage(dirtyArea);
                paintImmediately(dirtyArea);
//                repaintDirty();
            }

//            invalidateImage(dirtyArea);
//            immediatelyRepaintDirty();
//            repaintDirty(dirtyArea);
        }

    }


    // --- ChartContext implementation -----------------------------------------

    protected static class Context implements ChartContext {

        private ChartComponent chart;


        public Context(ChartComponent chart) { this.chart = chart; }

        protected ChartComponent getChartComponent() { return chart; }


        public boolean isRightBased() { return chart.isRightBased(); }

        public boolean isBottomBased() { return chart.isBottomBased(); }

        public boolean fitsWidth() { return chart.fitsWidth(); }

        public boolean fitsHeight() { return chart.fitsHeight(); }

        public long getDataOffsetX() { return chart.getDataOffsetX(); }

        public long getDataOffsetY() { return chart.getDataOffsetY(); }

        public long getDataWidth() { return chart.getDataWidth(); }

        public long getDataHeight() { return chart.getDataHeight(); }

        public long getViewWidth() { return chart.getContentsWidth(); }

        public long getViewHeight() { return chart.getContentsHeight(); }

        public long getViewportOffsetX() { return 0; }

        public long getViewportOffsetY() { return 0; }

        public int getViewportWidth() { return chart.getWidth(); }

        public int getViewportHeight() { return chart.getHeight(); }

        public double getViewX(double dataX) { return chart.getViewX(dataX); }

        public double getReversedViewX(double dataX) { return chart.getReversedViewX(dataX); }

        public double getViewY(double dataY) { return chart.getViewY(dataY); }

        public double getReversedViewY(double dataY) { return chart.getReversedViewY(dataY); }

        public double getViewWidth(double dataWidth) { return chart.getViewWidth(dataWidth); }

        public double getViewHeight(double dataHeight) { return chart.getViewHeight(dataHeight); }

        public LongRect getViewRect(LongRect dataRect) { return getViewRectImpl(dataRect); }

        public double getDataX(double viewX) { return chart.getDataX(viewX); }

        public double getReversedDataX(double viewX) { return chart.getReversedDataX(viewX); }

        public double getDataY(double viewY) { return chart.getDataY(viewY); }

        public double getReversedDataY(double viewY) { return chart.getReversedDataY(viewY); }

        public double getDataWidth(double viewWidth) { return chart.getDataWidth(viewWidth); }

        public double getDataHeight(double viewHeight) { return chart.getDataHeight(viewHeight); }


        private LongRect getViewRectImpl(LongRect dataRect) {
            LongRect viewRect = new LongRect();

            viewRect.x = (long)Math.ceil(getViewX(dataRect.x));
            viewRect.width = (long)Math.ceil(getViewWidth(dataRect.width));
            if (isRightBased()) viewRect.x -= viewRect.width;

            viewRect.y = (long)Math.ceil(getViewY(dataRect.y));
            viewRect.height = (long)Math.ceil(getViewHeight(dataRect.height));
            if (isBottomBased()) viewRect.y -= viewRect.height;

            return viewRect;
        }

    }

}
