/**
 *  Licensed under GPL. For more information, see
 *    http://jaxodraw.sourceforge.net/license.html
 *  or the LICENSE file in the jaxodraw distribution.
 */
package net.sf.jaxodraw.gui;

import java.awt.Color;
import java.awt.Cursor;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Image;
import java.awt.Insets;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.RenderingHints;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.FocusAdapter;
import java.awt.event.FocusEvent;
import java.awt.geom.GeneralPath;

import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;

import javax.swing.AbstractAction;
import javax.swing.KeyStroke;

import net.sf.jaxodraw.graph.JaxoGraph;
import net.sf.jaxodraw.object.JaxoExtendedObject;
import net.sf.jaxodraw.object.JaxoObject;
import net.sf.jaxodraw.object.JaxoObjectFactory;
import net.sf.jaxodraw.object.group.JaxoGroup;
import net.sf.jaxodraw.util.JaxoConstants;
import net.sf.jaxodraw.util.JaxoGeometry;
import net.sf.jaxodraw.util.graphics.JaxoGraphics2D;


/** An instance of the canvas: responsible for all the painting.
 * @since 2.0
 */
public final class JaxoCanvas extends AbstractJaxoCanvas {

    private static final long serialVersionUID = 7526471155622776147L;

    private static final String COMMIT_CHANGES = "commitGraphChanges";

    // Padding added to the bounding box (in image coordinates) to determine the
    // image area to be repainted
    //private static final Insets IMAGE_REPAINT_PADDING = new Insets(5, 5, 5, 5);
    // Padding added to graph bounding box to determine canvas size
    private static final int GRAPH_PADDING = 10;

    private static final int CROSS_LENGTH = 4; // make configurable as function of handle size?

    // Padding added to objects bounding boxes to determine whether
    // they are in the faint box
    private static final Insets FAINT_BOX_PADDING = new Insets(5, 5, 5, 5);
    private static final boolean PUT_DRAGGED_OBJECTS_TO_FOREGROUND = false;
    private final ActionListener actionListener; // receives ActionEvents from the Canvas
    private transient Image offScreenImage; // Image for double buffering
    private final Point fixed = new Point(); // Fixed point in graph when resizing
    private int curHandle; // Current handle
    private int currentMode = JaxoConstants.STANDBY; // synchronized with MainPanel

    // Faint Box variables
    private boolean fboxON;
    private final Point fboxOrigin = new Point(); // Fixed point in graph, only valid when fboxON
    private final Point fboxLocation = new Point(); // Moved point in graph, only valid when fboxON
    private static final Color FBOX_FILL_COLOR = new Color(1.f, 1.f, 1.f, .4f);
    private static final Color FBOX_LINE_COLOR = new Color(.5f, .5f, .5f, 1.f);

    /** indicates whether to paint the visual aid of objects. */
    private boolean visualAidPainted;

    /** variables for multi-point objects. */
    private final List<Point> points = new ArrayList<Point>(4);
    private final Point pointsAidLocation = new Point();
    private boolean pointsON;

    /** Resize groups by dragging variables. */
    private Rectangle rgbox;
    private float newRf = 1.0f;
    private int gHandle;
    private JaxoObject cachedGroup;
    private JaxoObject selectedObject;

    // Object that is not part of the background image, but painted in the foreground
    // - may (and now always is?) still be part of the graph. Typically, if it exists,
    // it is equal to the selected object of the graph. The extra reference is needed
    // because the selected object can be cleared anytime/outside, and the canvas needs
    // to know the state of the offscreen image (i.e. which 'hovering' object is missing.
    private JaxoObject hoveringObject;
    private boolean selectInside;

    // Cached bounds of the graph, which does /not/ include the hovering object.
    private transient Rectangle cachedGraphBounds;


    /**
     * Constructs a new canvas.
     *
     * @param listener An ActionListener to receive events from the Canvas.
     */
    public JaxoCanvas(final ActionListener listener) {
        super();
        this.actionListener = listener;

        setBackground(Color.gray);
        setForeground(Color.black);
        setOpaque(true);
        setFocusable(true);

        final Dimension d = getMinimumCanvasSize();
        JaxoGeometry.grow(d, getInsets());
        setPreferredSize(d);

        final JaxoCanvasEventListener eventListener = new JaxoCanvasEventListener(this);
        eventListener.setMode(currentMode);
        this.addMouseListener(eventListener);
        this.addMouseMotionListener(eventListener);
        this.addPropertyChangeListener(eventListener); // for mode changes

        setupPointMouseAction();

        getFboxPopup().addActionListener(new ActionListener() {
                public void actionPerformed(final ActionEvent e) {
                    actionListener.actionPerformed(e);
                    stopFaintBox();
                }
            });
    }

    private void fireActionPerformed(final String actionCommand) {
        fireActionPerformed(actionCommand, ActionEvent.ACTION_PERFORMED);
    }

    private void fireActionPerformed(final String actionCommand, final int actionId) {
        actionListener.actionPerformed(new ActionEvent(this, actionId, actionCommand));
    }

    /** {@inheritDoc} */
    public void markImageInvalid(final Rectangle boundingBox) {
        updateOffScreenImage(boundingBox);
    }

    /** {@inheritDoc} */
    public void refresh() {
        dropHoveringObject();
        setSelectedObject(null);
        markImageInvalid();
    }

    /**
     * Drop the current hovering object (put it back to the Image,
     * or into void if it is not part of the current graph).
     */
    private void dropHoveringObject() {
        if (hoveringObject != null) {
            final List<JaxoObject> l = getCanvasGraph().getObjectList();

            int index = l.lastIndexOf(hoveringObject);

            if (!l.isEmpty() && (index == (l.size() - 1))) {
                putObjectOnOffScreenImage(hoveringObject);
                hoveringObject = null;
            } else if (index == -1) {
                hoveringObject = null;
                // update the whole OSI, rescaling of groups doesn't work otherwise
                updateOffScreenImage(null);
            } else {
                Rectangle bounds = hoveringObject.getBounds();
                hoveringObject = null;
                updateOffScreenImage(bounds);
            }

            cachedGraphBounds = null;
        }
    }

    private void setHoveringObject(JaxoObject value) {
        if (hoveringObject != value) {
            dropHoveringObject();
            hoveringObject = value;
            cachedGraphBounds = null;

            if (getCanvasGraph().getObjectList().contains(hoveringObject)) {
                // Assume that the bounding box is valid -- only if contained
                markImageInvalid(hoveringObject.getBounds());
            } else {
                repaintObject(hoveringObject);
            }
        }
    }

    /** Sets the specified JaxoObject as currently selected.
     * @param newOb The JaxoObject to be set selected.
     */
    private void setSelectedObject(final JaxoObject newOb) {
        this.selectedObject = newOb;
    }

    /** Sets the current graph. Also clears the hovering object.
     * @param value The graph to be set.
     */
    @Override
    public void setCanvasGraph(final JaxoGraph value) {
        if (getCanvasGraph() != value) {
            //Rectangle oldBounds = null;
            //Rectangle newBounds = null;
            //if (canvasGraph != null) {
            //    oldBounds = canvasGraph.getBounds();
            //}
            super.setCanvasGraph(value);

            //if (canvasGraph != null) {
            //    newBounds = canvasGraph.getBounds();
            //}
            //Rectangle damagedBounds =
            //    (oldBounds == null) ? newBounds
            //                        : (
            //        (newBounds == null) ? oldBounds : newBounds.union(oldBounds)
            //    );
            if (hoveringObject != null) {
                // Assume that the bounding box is valid
                //damagedBounds =
                //    damagedBounds.union(hoveringObject.getBounds());
                hoveringObject = null;
            }

            // markImageInvalid(damagedBounds);
            // LEGACY:
            markImageInvalid();

            if ((getHandlePaintMode() != HANDLE_PAINT_OFF) || visualAidPainted) {
                repaint();
            }

            revalidateCanvas();
        }
    }

    /** Ensure the off screen image exists and is up-to-date. */
    private void ensureValidOffScreenImage() {
        // currently synchronous updates - so the only way it can
        // not be up-to-date is when it does not exist.
        if (offScreenImage == null) {
            createOffScreenImage();
            updateOffScreenImage(null);
        }
    }

    /**
     * Paint the Canvas.
     *
     * @param g The graphics context to paint to.
     */
    @Override
    protected void paintComponent(final Graphics g) {
        final JaxoGraphics2D g2 = new JaxoGraphics2D((Graphics2D) g.create());

        final Insets n = getInsets();

        ensureValidOffScreenImage();

        final Dimension d = getSize();
        final Dimension canvasSize = getCanvasSize();

        if (d.width != canvasSize.width || d.height != canvasSize.height) {
            g2.setColor(getBackground());
            g2.fillRect(0, 0, getWidth(), getHeight());
        }

        g2.translate(n.left, n.top);
        g2.clipRect(0, 0, canvasSize.width, canvasSize.height);
        g2.drawImage(offScreenImage, 0, 0, null);

        if (hoveringObject != null) {
            final Object old = g2.getRenderingHint(RenderingHints.KEY_ANTIALIASING);

            final Object oldControl =
                g2.getRenderingHint(RenderingHints.KEY_STROKE_CONTROL);

            g2.setRenderingHint(RenderingHints.KEY_STROKE_CONTROL,
                RenderingHints.VALUE_STROKE_PURE);

            // This is slower, but otherwise the object looks noticably different.
            if (isAntialiasEnabled()) {
                g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
                    RenderingHints.VALUE_ANTIALIAS_ON);
            }

            hoveringObject.paint(g2);

            if (isAntialiasEnabled()) {
                g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, old);
            }

            g2.setRenderingHint(RenderingHints.KEY_STROKE_CONTROL, oldControl);
        }

        if (visualAidPainted && selectedObject != null) {
            g2.setColor(DEFAULT_HELP_COLOR);
            g2.setStroke(DEFAULT_HELP_STROKE);

            selectedObject.paintVisualAid(g2);
        }

        if (getHandlePaintMode() == HANDLE_PAINT_ON) {
            // SELECT? at least do not use currentMode for fbox handles
            final int handleMode = fboxON ? JaxoConstants.SELECT : currentMode;
            getCanvasGraph().paintHandles(g2, getHandle(), handleMode);
        } else if (getHandlePaintMode() == HANDLE_PAINT_SELECTION) {
            selectedObject.paintHandles(g2, getHandle(), currentMode);
        }

        if (fboxON) {
            paintFaintBox(g2);
        }

        if (pointsON) {
            paintPointsAid(g2);
        }

        g2.dispose();
    }

    /**
     * Print the Canvas.
     *
     * @param gr The graphics context to print to.
     */
    @Override
    protected void printComponent(final Graphics gr) {
        final JaxoGraphics2D g = new JaxoGraphics2D((Graphics2D) gr.create());
        g.setPrinting(true);

        g.setColor(getBackground());
        g.fillRect(0, 0, getWidth(), getHeight());

        final Insets n = getInsets();
        g.translate(n.left, n.top);

        paintBackgroundAndGrid(g, new Rectangle(getCanvasSize()));

        if (isAntialiasEnabled()) {
            g.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
                RenderingHints.VALUE_ANTIALIAS_ON);
        }

        g.setRenderingHint(RenderingHints.KEY_STROKE_CONTROL,
            RenderingHints.VALUE_STROKE_PURE);

        getCanvasGraph().paintClipped(g);

        g.dispose();
    }

    /** {@inheritDoc} */
    protected void rebuildImage() {
        if (offScreenImage != null) {
            offScreenImage.flush();
            offScreenImage = null;
        }

        ensureValidOffScreenImage();
    }

    /** Create a new offScreenImage (with undefined contents). */
    private void createOffScreenImage() {
        final Dimension d = getEffectiveMaximumCanvasSize();
        offScreenImage = createImage(d.width, d.height);
        getZoom().setBackground(offScreenImage);
    }

    /**
     * Updates the offScreenImage within the given clipping area.
     *
     * @param clip the clipping area.
     * May be null in which case the whole offScreenImage is updated.
     */
    private void updateOffScreenImage(final Rectangle oldbb) {
        if (offScreenImage == null) {
            return;
        }

        final JaxoGraphics2D g2 =
            new JaxoGraphics2D((Graphics2D) offScreenImage.getGraphics());

        if (isAntialiasEnabled()) {
            g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
                RenderingHints.VALUE_ANTIALIAS_ON);
        }

        g2.setRenderingHint(RenderingHints.KEY_STROKE_CONTROL,
            RenderingHints.VALUE_STROKE_PURE);

        Rectangle damagedBounds;

        if (oldbb == null) {
            damagedBounds =
                new Rectangle(0, 0, offScreenImage.getWidth(null),
                    offScreenImage.getHeight(null));
        } else {
            damagedBounds = oldbb;
        }

        paintBackgroundAndGrid(g2, damagedBounds);

        g2.clip(damagedBounds);

        if (hoveringObject == null) {
            getCanvasGraph().getObjectList().paintClipped(g2);
        } else {
            getCanvasGraph().getObjectList().paintClippedExcept(
                Collections.singleton(hoveringObject), g2);
        }

        g2.dispose();

        repaintBoundingBox(damagedBounds);
    }

    private void repaintHandles(final JaxoObject o) {
        repaintBoundingBox(o.getBoundsWithHandles(getHandle()));
    }

    private void repaintObject(JaxoObject o) {
        repaintBoundingBox(o.getBounds());
    }

    /** Determines which objects should be selected as belonging to a group (ie, whose
     * handles are to be painted gray), when the mouse is dragged withthe middle or
     * right button.
     * @param p The point in graph coordinates
     */
    private void selectObjectsToGroup(final Point p) {
        JaxoObject newSelectedOb = null;
        int i = getCanvasGraph().listSize();

        setSelectedObject(null);

        while ((newSelectedOb == null) && (i >= 1)) {
            i--;

            final JaxoObject curOb = getCanvasGraph().listElementAt(i);

            final int grabbedHandle = curOb.getGrabbedHandle(p.x, p.y, getHandle());

            if (curOb.canBeSelected(grabbedHandle, currentMode)) {
                newSelectedOb = curOb;
                newSelectedOb.setAsMarked(!newSelectedOb.isMarked());
                setSelectedObject(curOb);
                repaintHandles(newSelectedOb);
            }
        }
    }

    /** Select the object when the mouse is clicked or pressed by comparing
     * to see if the mouse event occured in a handle
     * of an object. Loop through the current list of objects from the
     * end to the beginning. If the mouse is in the handle of more
     * than one object, then the top object will be selected.
     * @param p The point in graph coordinates
     * @param editmode The current edit mode
     */
    private void selectObject(final Point p, final int editmode) {
        JaxoObject newSelectedOb = null;
        int i = getCanvasGraph().listSize();

        while ((newSelectedOb == null) && (i >= 1)) {
            i--;

            final JaxoObject curOb = getCanvasGraph().listElementAt(i);

            if (curOb != null) {
                final int grabbedHandle = curOb.getGrabbedHandle(p.x, p.y, getHandle());

                if (curOb.canBeSelected(grabbedHandle, editmode)) {
                    newSelectedOb = curOb;
                    setSelectedObject(newSelectedOb);
                    if (editmode == JaxoConstants.RESIZE) {
                        curHandle = grabbedHandle;
                        setStartCoordinates();
                    }
                }
            }
        }
        setSelectedObject(newSelectedOb);
    }

    /** Set the fixed point for resizing an object.
     * This is always the point opposite to the grabbed handle
     * (Usually the center point (x,y), except for lines it
     * may be the end point (x+relw,y+relh) if the origin is grabbed.
     * For boxes and groups it can also be one of the diagonal points.)
     */
    private void setStartCoordinates() {
        if (selectedObject == null) {
            return;
        }

        final JaxoExtendedObject selectedOb = (JaxoExtendedObject) selectedObject;

        fixed.x = selectedOb.getX();
        fixed.y = selectedOb.getY();
        if (curHandle == JaxoExtendedObject.SELECT_P1) {
            fixed.x = selectedOb.getX2();
            fixed.y = selectedOb.getY2();
        } else if (curHandle == JaxoExtendedObject.SELECT_DX) {
            fixed.x = selectedOb.getX2();
        } else if (curHandle == JaxoExtendedObject.SELECT_DY) {
            fixed.y = selectedOb.getY2();
        }
    }

    private void finishModeDragAction() {
        finishModeDragAction(false);
    }

    private void stopModeDragAction() {
        finishModeDragAction(true);
    }

    private void finishModeDragAction(final boolean cancel) {
        if (fboxON) {
            return;
        }

        //lastDraggedEvent = null;
        //scrollTimer.stop();

        boolean resetMode = false;

        if ((currentMode == JaxoConstants.MOVE)
                || (currentMode == JaxoConstants.DUPLICATE)
                || (currentMode == JaxoConstants.RESIZE)) {
            if (selectedObject != null) {
                dropHoveringObject();
                setVisualAidPainted(false);
                setHandlePaintMode(HANDLE_PAINT_ON);
                revalidateCanvas();

                fireActionPerformed(COMMIT_CHANGES);

                resetMode = true;
            }
        } else if (JaxoConstants.isParticleMode(currentMode)
                || (currentMode == JaxoConstants.BOX)
                || (currentMode == JaxoConstants.BLOB)
                || (currentMode == JaxoConstants.ZIGZAG)
                || JaxoConstants.isVertexMode(currentMode)) {
            if (selectedObject != null) {
                boolean empty = false;

                // Do not allow newly created objects to have size (0, 0)
                if (selectedObject.getPointCount() != 1) {
                    empty = (selectedObject.getWidth() == 0)
                            && (selectedObject.getHeight() == 0);
                }

                if (empty) {
                    getCanvasGraph().delete(selectedObject);
                } else {
                    fireActionPerformed("commitRepeatableGraphChanges");
                }

                dropHoveringObject();
                setVisualAidPainted(false);
                revalidateCanvas();

                resetMode = true;
            }
        } else if (JaxoConstants.isSelectMode(currentMode)) {
            if ((selectedObject != null) && selectedObject.isMarked() && !cancel) {
                if (currentMode == JaxoConstants.UNGROUP) {
                    if (selectedObject instanceof JaxoGroup) {
                        ungroupSelectedObject();
                    }

                    resetMode = true;
                } else if (currentMode == JaxoConstants.COLOR) {
                    selectedObject.setAsMarked(false);
                    setHandlePaintMode(HANDLE_PAINT_SELECTION);
                    showColorChooser(selectedObject);
                    resetMode = true;
                } else if (currentMode == JaxoConstants.DELETE) {
                    getCanvasGraph().delete(selectedObject);
                    fireActionPerformed(COMMIT_CHANGES);
                    // Assume that the bounding box is valid
                    markImageInvalid(selectedObject.getBounds());
                    revalidateCanvas();

                    resetMode = true;
                } else if (currentMode == JaxoConstants.BACKGROUND) {
                    getCanvasGraph().background(selectedObject);
                    fireActionPerformed(COMMIT_CHANGES);
                    // would not the selected object suffice?
                    markImageInvalid(getCanvasGraph().getBounds());

                    resetMode = true;
                } else if (currentMode == JaxoConstants.FOREGROUND) {
                    getCanvasGraph().foreground(selectedObject);
                    fireActionPerformed(COMMIT_CHANGES);
                    // see above
                    markImageInvalid(getCanvasGraph().getBounds());
                    resetMode = true;
                } else if (currentMode == JaxoConstants.EDIT) {
                    selectedObject.setAsMarked(false);
                    showEditPanelFor(selectedObject);
                }

                if (currentMode != JaxoConstants.SELECT) {
                    // for these, selection stays marked
                    if (selectedObject != null) {
                        selectedObject.setAsMarked(false);
                        repaintHandles(selectedObject);
                    }
                }
            }

            selectInside = false;
        }

        if (resetMode) {
            fireActionPerformed("resetMode");
        }
    }

    /** Start faint box at 'p'. If already active, restart at 'p'.
     * @param p The point to start.
     */
    private void startFaintBox(final Point p) {
        fboxON = true;
        fboxOrigin.setLocation(p);
        fboxLocation.setLocation(p);

        repaint();
    }

    /** Drag faint box to 'p', starting if 'p' if not already on.
     * @param p The point to start.
     */
    private void dragFaintBox(final Point p) {
        fboxLocation.setLocation(p);

        // To avoid interference with right-double-click (flickering),
        // these are only set with drag (even if to the same position).
        setCursor(Cursor.getPredefinedCursor(Cursor.SE_RESIZE_CURSOR));
        setHandlePaintMode(HANDLE_PAINT_ON);
        setVisualAidPainted(false);

        updateFaintBoxContents();
        repaint();
    }

    /** Stop faint box if active; returning to the current mode (initial state). */
    private void stopFaintBox() {
        if (fboxON) {
            fboxON = false;
            getCanvasGraph().setAsMarked(false);

            repaint();
        }
    }

    /** Make (exactly) the objects contained with the 'faintBoxBounds'
     * 'marked'. Does not repaint anything.
     */
    private void updateFaintBoxContents() {
        final Rectangle faintbox = getFaintBoxBounds();

        for (int i = 0; i < getCanvasGraph().listSize(); i++) {
            final JaxoObject jaxoOb = getCanvasGraph().listElementAt(i);
            final Rectangle bounds = jaxoOb.getBounds();
            JaxoGeometry.grow(bounds, FAINT_BOX_PADDING);

            if (faintbox.contains(bounds)) {
                jaxoOb.setAsMarked(true);
            } else {
                jaxoOb.setAsMarked(false);
            }
        }
    }

    /** Bounds of the faintBox. */
    private Rectangle getFaintBoxBounds() {
        final int width = fboxLocation.x - fboxOrigin.x;
        final int height = fboxLocation.y - fboxOrigin.y;

        Rectangle faintbox;

        if (height < 0) {
            if (width < 0) {
                faintbox = new Rectangle(fboxLocation.x, fboxLocation.y, -width, -height);
            } else {
                faintbox = new Rectangle(fboxOrigin.x, fboxLocation.y, width, -height);
            }
        } else {
            if (width < 0) {
                faintbox = new Rectangle(fboxLocation.x, fboxOrigin.y, -width, height);
            } else {
                faintbox = new Rectangle(fboxOrigin.x, fboxOrigin.y, width, height);
            }
        }

        return faintbox;
    }

    /** Paints a light gray box that is used for grouping objects.
     * @param g2 The graphics context to paint
     */
    private void paintFaintBox(final JaxoGraphics2D g2) {
        final Rectangle f = getFaintBoxBounds();
        g2.setColor(FBOX_FILL_COLOR);
        g2.fill(f);
        g2.draw(f);
        g2.setColor(FBOX_LINE_COLOR);
        g2.setStroke(DEFAULT_HELP_STROKE);
        g2.draw(f);
    }

    /** Sets whether the visual aid should be painted.
     * @param value True if the visual aid should be drawn, false otherwise.
     */
    private void setVisualAidPainted(final boolean value) {
        if (visualAidPainted != value) {
            this.visualAidPainted = value;

            if (selectedObject != null) {
                repaint();
            }
        }
    }

    /** Setup the main panel glassPane to allow swallowing MouseEvents outside
     * the JaxoCanvas.
     */
    private void setupPointMouseAction() {
        getInputMap().put(KeyStroke.getKeyStroke("ESCAPE"), "stopPointMouseAction");
        getActionMap().put("stopPointMouseAction",
            new AbstractAction() {
                private static final long serialVersionUID = 75264711556226147L;

                public void actionPerformed(final ActionEvent e) {
                    stopPointMouseAction();
                }

            @Override
                public boolean isEnabled() {
                    return pointsON;
                }
            });

        addFocusListener(new FocusAdapter() {
            @Override
                public void focusLost(final FocusEvent e) {
                    stopPointMouseAction();
                }
            });
    }

    /** When drawing arc objects etc.: start click-and-mouse mode.
     * This is called from createNewObject.
     */
    private void startPointMouseAction() {
        if (!pointsON) {
            pointsON = true;
            pointsAidLocation.setLocation(points.get(0));
            requestFocusInWindow();
            fireActionPerformed("setGlassPaneVisible");
            revalidateCanvas();
            repaint();
        }
    }

    /** When drawing arc objects etc.: stop click-and-mouse mode.
     * This is called from createNewObject (when the new object has
     * been built), and also otherwise to extraordinarily stop
     * click-and-move mode.
     */
    private void stopPointMouseAction() {
        if (pointsON) {
            pointsON = false;
            points.clear();
            fireActionPerformed("setGlassPaneInvisible");
            revalidateCanvas();
            repaint();
        }
    }

    private void paintPointsAid(final JaxoGraphics2D g2) {
        g2.setColor(DEFAULT_HELP_COLOR);
        g2.setStroke(DEFAULT_HELP_STROKE);

        final Point first = points.get(0);

        paintCrossAt(g2, first.x, first.y);

        final GeneralPath gp = new GeneralPath();

        gp.moveTo(first.x, first.y);

        for (final Iterator<Point> i = points.listIterator(1); i.hasNext();) {
            final Point p = i.next();
            gp.lineTo(p.x, p.y);
        }

        gp.lineTo(pointsAidLocation.x, pointsAidLocation.y);

        g2.draw(gp);
    }

    private void paintCrossAt(final JaxoGraphics2D g, final int x, final int y) {
        g.drawLine(x - CROSS_LENGTH, y - CROSS_LENGTH, x + CROSS_LENGTH, y + CROSS_LENGTH);
        g.drawLine(x - CROSS_LENGTH, y + CROSS_LENGTH, x + CROSS_LENGTH, y - CROSS_LENGTH);
    }

    private JaxoObject createNewObject(final Point q) {

        // note: we create an object for every mouse click, this would not
        // be necessary if we knew the pointCount for a given currentMode
        // ie without having to instantiate the object
        JaxoObject newob = JaxoObjectFactory.newObject(currentMode);

        final int pointCount = newob.getPointCount();
        final Point[] location = new Point[pointCount];

        if (pointCount == 1) {
            location[0] = q;
        } else if (pointCount == 2) {
            location[0] = q;
            location[1] = q;
        } else {
            points.add(new Point(q));
            if (points.size() == pointCount) {
                for (int i = 0; i < pointCount; i++) {
                    location[i] = points.get(i);
                }
                stopPointMouseAction();
            } else {
                startPointMouseAction();
                return null;
            }
        }

        newob.setPoints(location);
        // let preferences override location, eg to set minimum size
        newob.setPreferences();

        if (JaxoConstants.isTextMode(currentMode)) {
            final String text = JaxoDialogs.getText(asComponent());
             newob.setParameter("textString", text);
            if (text.length() == 0) {
                newob = null;
            }
        }

        return newob;
    }

    private void showColorChooser(final JaxoObject ob) {
        if (HOVERING_EDITED_OBJECTS) {
            setHoveringObject(ob);
        }

        final boolean hasChanged = showColorPanel(ob);

        if (HOVERING_EDITED_OBJECTS) {
            dropHoveringObject();
        } else {
            // Assume that the bounding box is valid - really should be
            markImageInvalid(ob.getBounds());
        }

        if (hasChanged) {
            fireActionPerformed(COMMIT_CHANGES);
        }
    }

    // Paint 'ob' on top of the background image. This assumes that
    // 'ob' was previously hovering (i.e. is not already on the background
    // image), and that it is in the foreground of the graph - CALL IT ONLY
    // IF YOU ARE REALLY SURE, otherwise the background image may be corrupted.
    // (This is just an optimization to repainting the complete background
    // image within the bounds of 'ob'.)
    private void putObjectOnOffScreenImage(JaxoObject ob) {
        JaxoGraphics2D g2 =
            new JaxoGraphics2D((Graphics2D) offScreenImage.getGraphics());

        if (isAntialiasEnabled()) {
            g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
                RenderingHints.VALUE_ANTIALIAS_ON);
        }

        g2.setRenderingHint(RenderingHints.KEY_STROKE_CONTROL,
            RenderingHints.VALUE_STROKE_PURE);

        ob.paint(g2);
        g2.dispose();

        repaintBoundingBox(ob.getBounds());
    }

    private void setCachedGroup(final JaxoGroup group) {
        cachedGroup = group.copy();
    }

    /* Methods for rescaling groups by dragging. */
    private void swapGroups(final JaxoGroup scaledGroup, final JaxoGroup copyGroup) {
        getCanvasGraph().delete(scaledGroup);
        final JaxoObject scalableGroup = copyGroup.copy();
        getCanvasGraph().addObject(scalableGroup);
        setSelectedObject(scalableGroup);
    }

    private void setFixedPointAndScaleFactor(final int selectedHandle, final int xpos) {
        if (selectedHandle == JaxoExtendedObject.SELECT_P1) {
            fixed.x = rgbox.x + rgbox.width;
            fixed.y = rgbox.y + rgbox.height;
            if (xpos < fixed.x) {
                newRf = Math.abs((float) (xpos - fixed.x) / rgbox.width);
            }
        } else if (selectedHandle == JaxoExtendedObject.SELECT_DY) {
            fixed.x = rgbox.x;
            fixed.y = rgbox.y + rgbox.height;
            if (xpos > fixed.x) {
                newRf = Math.abs((float) (xpos - fixed.x) / rgbox.width);
            }
        } else if (selectedHandle == JaxoExtendedObject.SELECT_P2) {
            fixed.x = rgbox.x;
            fixed.y = rgbox.y;
            if (xpos > fixed.x) {
                newRf = Math.abs((float) (xpos - fixed.x) / rgbox.width);
            }
        } else if (selectedHandle == JaxoExtendedObject.SELECT_DX) {
            fixed.x = rgbox.x + rgbox.width;
            fixed.y = rgbox.y;
            if (xpos < fixed.x) {
                newRf = Math.abs((float) (xpos - fixed.x) / rgbox.width);
            }
        }
        if (newRf < 0.1f) {
            newRf = 0.1f;
        }
    }

    /** Resets the preferred size of the canvas, fast
        (avoid calling canvasGraph.getBounds(), but use a cached value).
        This assumes only the 'hoveringObject' or the 'pointsAidLocation' may
        have changed bounds.
     */
    /* package */ void revalidateCanvasFast() {
        if (cachedGraphBounds == null) {
            revalidateCanvas();
        } else {
            continueRevalidate(new Rectangle(cachedGraphBounds));
        }
    }

    /** {@inheritDoc} */
    protected void revalidateCanvas() {
        Rectangle bBox;

        if (hoveringObject == null) {
            bBox = getCanvasGraph().getBounds();
        } else {
            bBox = getCanvasGraph().getBoundsExcept(
                Collections.singleton(hoveringObject));
        }

        if (bBox == null) {
            bBox = new Rectangle();
        }

        cachedGraphBounds = new Rectangle(bBox);

        continueRevalidate(bBox);
    }

    // continue revalidateCanvas(Fast). 'bBox' is the graph bounds,
    // without hoveringObject or pointsAidLocation.
    private void continueRevalidate(final Rectangle bBox) {
        final Dimension maximum = getEffectiveMaximumCanvasSize();

        if (hoveringObject != null) {
            bBox.add(hoveringObject.getBounds());
        }

        if (pointsON) {
            for (final Iterator<Point> i = points.iterator(); i.hasNext();) {
                bBox.add(i.next());
            }
            bBox.add(pointsAidLocation);
        }

        final int graphWidth = (bBox.x + bBox.width) + GRAPH_PADDING;
        final int graphHeight = (bBox.y + bBox.height) + GRAPH_PADDING;

        final Insets n = getInsets();

        final int newWidth =
            valueBetween(getMinimumCanvasSize().width, graphWidth, maximum.width)
            + n.left + n.right;
        final int newHeight =
            valueBetween(getMinimumCanvasSize().height, graphHeight, maximum.height)
            + n.top + n.bottom;

        final Dimension d = getPreferredSize();

        if ((d.width != newWidth) || (d.height != newHeight)) {
            setPreferredSize(new Dimension(newWidth, newHeight));
            revalidate();
        }
    }

    // 'value' put to the closest allowed value in [minimum, maximum]
    // requires: minimum <= maximum
    private static int valueBetween(final int minimum, final int value, final int maximum) {
        return Math.max(minimum, Math.min(value, maximum));
    }

    /** {@inheritDoc} */
    protected void deleteMarkedObjects() {
        final Rectangle oldBounds = getCanvasGraph().getBounds();
        getCanvasGraph().deleteMarkedObjects();
        fireActionPerformed(COMMIT_CHANGES);
        markImageInvalid(oldBounds);
        revalidateCanvas();
    }

    /** Paste the objects currently on the clipboard. */
    public void pasteFromClipboard() {
        final JaxoGraph g = getSystemClipboardGraph();

        if (g != null) {
            paste(g);
        }

        dropHoveringObject();
        setSelectedObject(null);
    }

    /**
     * Paste a graph.
     *
     * @param g The graph to paste.
     */
    private void paste(final JaxoGraph g) {
        final int[] delta = getPastingDelta(g);
        for (int k = 0; k < g.listSize(); k++) {
            final JaxoObject obj = g.listElementAt(k).copy();
            obj.moveBy(delta[0], delta[1]);
            getCanvasGraph().addObject(obj);
        }

        fireActionPerformed("commitRepeatableGraphChangesN", g.listSize());
        markImageInvalid();
        revalidateCanvas();
    }

    private int[] getPastingDelta(final JaxoGraph g) {
        int[] delta = {0, 0};

        if (fboxON) {
            final Rectangle bbox = g.getBounds();
            final Rectangle f = getFaintBoxBounds();

            delta[0] = f.x - bbox.x;
            delta[1] = f.y - bbox.y;
        }

        return delta;
    }

    /** {@inheritDoc} */
    public void ungroupMarkedObjects() {
        if (getCanvasGraph().ungroupMarkedObjects()) {
            fireActionPerformed(COMMIT_CHANGES);

            if (getHandlePaintMode() != HANDLE_PAINT_OFF) {
                repaint();
            }
        }
    }

    /** If the selected object is a group, ungroup it. */
    public void ungroupSelectedObject() {
        if (getCanvasGraph().ungroup(selectedObject)) {
            if (getHandlePaintMode() != HANDLE_PAINT_OFF) {
                repaint();
            }

            setSelectedObject(null); // group has gone
            fireActionPerformed(COMMIT_CHANGES);
        }
    }

    /** {@inheritDoc} */
    public void groupMarkedObjects() {
        if (getCanvasGraph().groupMarkedObjects()) {
            if (getHandlePaintMode() != HANDLE_PAINT_OFF) {
                repaint();
            }

            // Marks (only) adding the group as repeatable!
            fireActionPerformed("commitRepeatableGraphChanges");
        }
    }

    /** {@inheritDoc} */
    public void updateMode(final int mode) {
        this.currentMode = mode;

        stopPointMouseAction();
        dropHoveringObject();
        setSelectedObject(null);

        setCursor(Cursor.getPredefinedCursor(Cursor.CROSSHAIR_CURSOR));
        setHandlePaintMode(HANDLE_PAINT_OFF);
        setVisualAidPainted(false);

        if (JaxoConstants.isMiscMode(mode)) {
            if ((mode == JaxoConstants.TEXT) || (mode == JaxoConstants.LATEX)) {
                setCursor(Cursor.getPredefinedCursor(Cursor.TEXT_CURSOR));
            }
        } else if (JaxoConstants.isEditMode(mode)) {
            if ((mode != JaxoConstants.MOVE) && (mode != JaxoConstants.RESIZE)
                    && (mode != JaxoConstants.DUPLICATE)) {
                setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR));
            }
            setHandlePaintMode(HANDLE_PAINT_ON);
        }
    }

    /** {@inheritDoc} */
    public void clear() {
        dropHoveringObject();
        getCanvasGraph().clear();
        fireActionPerformed(COMMIT_CHANGES);
        setSelectedObject(null);
        markImageInvalid();
        revalidateCanvas();
    }

    /** {@inheritDoc} */
    public void moveSelection(final boolean backGround) {
        dropHoveringObject();
        setSelectedObject(null);
        if (backGround) {
            getCanvasGraph().objectsToBackground();
        } else {
            getCanvasGraph().objectsToForeground();
        }
        fireActionPerformed(COMMIT_CHANGES);
        markImageInvalid();
    }

    /** {@inheritDoc} */
    public void editNearestObject(final Point p) {
        stopFaintBox();
        stopPointMouseAction();
        stopModeDragAction(); // WHAT FOR?

        setSelectedObject(getCanvasGraph().getNearestObject(p.x, p.y));
        showEditPanelFor(selectedObject);
    }

    private void showEditPanelFor(final JaxoObject o) {
        if (o == null) {
            return;
        }

        if (HOVERING_EDITED_OBJECTS) {
            setHoveringObject(o);
        }

        final boolean hasChanged = showEditPanel(o);

        if (HOVERING_EDITED_OBJECTS) {
            dropHoveringObject();
        }

        if (hasChanged) {
            fireActionPerformed(COMMIT_CHANGES);
            revalidateCanvas();
        }

        fireActionPerformed("resetMode");
    }

    /** {@inheritDoc} */
    public void initiateEdit(final Point p) {
        if (currentMode == JaxoConstants.STANDBY) {
            return;
        } else if ((currentMode == JaxoConstants.MOVE)
                || (currentMode == JaxoConstants.RESIZE)) {
            selectObject(p, currentMode);

            if (selectedObject != null) {
                setHandlePaintMode(HANDLE_PAINT_OFF);
                setVisualAidPainted(true);

                if (selectedObject instanceof JaxoGroup) {
                    setCachedGroup((JaxoGroup) selectedObject);
                    gHandle = curHandle;
                    rgbox = ((JaxoGroup) selectedObject).getBounds();
                }

                setHoveringObject(selectedObject);

                if (currentMode == JaxoConstants.MOVE) {
                    snapObject(selectedObject);
                }
            }
        } else if (currentMode == JaxoConstants.DUPLICATE) {
            selectObject(p, JaxoConstants.MOVE);

            if (selectedObject != null) {
                setHandlePaintMode(HANDLE_PAINT_OFF);
                setVisualAidPainted(true);

                final JaxoObject copyOb = selectedObject.copy();
                getCanvasGraph().addObject(copyOb);
                setSelectedObject(copyOb);

                snapObject(copyOb);
                setHoveringObject(copyOb);
            }
        } else if (JaxoConstants.isSelectMode(currentMode)) {
            if (currentMode == JaxoConstants.SELECT) {
                selectObjectsToGroup(p);
                selectInside = (selectedObject != null) && selectedObject.isMarked();
            } else {
                selectObject(p, JaxoConstants.MOVE);
                selectInside = true;

                if (selectedObject != null) {
                    selectedObject.setAsMarked(true);
                    repaintHandles(selectedObject);
                }
            }
            return;
        } else {
            final Point q = new Point(p);

            snapPoint(q);

            final JaxoObject newob = createNewObject(q);

            if (newob == null) {
                if (pointsON) {
                    repaint(); // for points
                }
            } else {
                // Add new object to the list and set it to be selected object
                getCanvasGraph().addObject(newob);
                setSelectedObject(newob);
                setHoveringObject(newob);

                //text and latex objects are special, as they have no Canvas release event: back-up them here.
                if (JaxoConstants.isTextMode(currentMode)) {
                    // use setHoveringObject/dropHoveringObject anyway because
                    // they will repaint nicely
                    dropHoveringObject();
                    fireActionPerformed("commitRepeatableGraphChanges");
                    fireActionPerformed("resetMode");
                } else {
                    setHandlePaintMode(HANDLE_PAINT_OFF);
                    setVisualAidPainted(true);
                    // assume, for the moment, that the points are in order
                    curHandle = newob.getPointCount() - 1;
                    setStartCoordinates();
                }

                revalidateCanvas();
                repaint();
            }
        }
    }

    /** {@inheritDoc} */
    public void continueEdit(final Point p, final Point last) {
        if (pointsON) {
            final Point q = points.get(points.size() - 1);
            q.setLocation(p);
            updatePointsAid(q);

            return;
        }

        if (selectedObject == null) {
            return;
        }

        if ((currentMode == JaxoConstants.RESIZE)
                || JaxoConstants.isNewObjectMode(currentMode)) {
            if (PUT_DRAGGED_OBJECTS_TO_FOREGROUND) {
                getCanvasGraph().foreground(selectedObject);
            }

            final Point q = new Point(p);

            snapPoint(q);

            if (selectedObject instanceof JaxoGroup) {
                swapGroups((JaxoGroup) selectedObject,
                    (JaxoGroup) cachedGroup);
                setFixedPointAndScaleFactor(gHandle, q.x);
                final JaxoGroup group = (JaxoGroup) selectedObject;
                group.rescaleObject(fixed.x, fixed.y, newRf);

                // Switch under the hood
                if (hoveringObject == selectedObject) {
                    hoveringObject = group;
                }
            }

            if (curHandle >= 0) {
                selectedObject.setX(curHandle, q.x);
                selectedObject.setY(curHandle, q.y);
            } else {
                if (curHandle == JaxoExtendedObject.SELECT_DX) {
                    selectedObject.setLocation(q.x, fixed.y);
                    ((JaxoExtendedObject) selectedObject).setRelWAndH(fixed.x
                        - q.x, q.y - fixed.y);
                } else if (curHandle == JaxoExtendedObject.SELECT_DY) {
                    selectedObject.setLocation(fixed.x, q.y);
                    ((JaxoExtendedObject) selectedObject).setRelWAndH(q.x
                        - fixed.x, fixed.y - q.y);
                }
            }

            repaint();
        } else if ((currentMode == JaxoConstants.MOVE)
                || (currentMode == JaxoConstants.DUPLICATE)) {
            if (PUT_DRAGGED_OBJECTS_TO_FOREGROUND) {
                getCanvasGraph().foreground(selectedObject);
            }

            final Point m = new Point(last);
            final Point q = new Point(p);

            snapObject(selectedObject);

            snapPoint(m);
            snapPoint(q);
            selectedObject.moveBy(q.x - m.x, q.y - m.y);

            repaint();
        } else if (JaxoConstants.isSelectMode(currentMode)) {
            final int grabbedHandle =
                selectedObject.getGrabbedHandle(p.x, p.y, getHandle());

            final boolean inside =
                (grabbedHandle != -1)
                && selectedObject.canBeSelected(grabbedHandle, currentMode);

            if ((inside == selectInside) != selectedObject.isMarked()) {
                selectedObject.setAsMarked(inside == selectInside);
                repaintHandles(selectedObject);
            }
        }
    }

    /** {@inheritDoc} */
    public void finalizeEdit() {
        if (!pointsON) {
            finishModeDragAction();
            updateMode(currentMode);
        }
    }

    /** {@inheritDoc} */
    public void updatePointsAid(final Point p) {
        if (pointsON) {
            pointsAidLocation.setLocation(p);
            snapPoint(pointsAidLocation);
            repaint();
        }
    }

    /** {@inheritDoc} */
    public void initiateSelect(final Point p) {
        startFaintBox(p);
    }

    /** {@inheritDoc} */
    public void continueSelect(final Point p) {
        dragFaintBox(p);
    }

    /** {@inheritDoc} */
    public void finalizeSelect(final Point p) {
        popupSelectionPanel(p);
        updateMode(currentMode);
    }

    private void popupSelectionPanel(final Point location) {
        if (!showSelectionPanel(location)) {
            stopFaintBox();
        }
    }
}
