// Charles A. Loomis, Jr., and University of California, Santa Cruz,
// Copyright (c) 2000
package org.freehep.swing.graphics;

import java.awt.*;
import java.awt.event.KeyEvent;
import java.awt.event.MouseEvent;
import java.awt.geom.AffineTransform;
import java.util.Arrays;

import java.awt.image.BufferedImage;

import javax.swing.SwingUtilities;

import org.freehep.swing.images.FreeHepImage;

/**
 * This panel allows the user to select a point on the screen.  The
 * crosshair cursor fill follow the system pointer when the left mouse
 * button is pressed.  The system pointer will become invisible while
 * the user is actively repositioning the crosshair.  
 *
 * This panel will also respond to keystroke input.
 * <ul>
 * <li>arrow keys: move the active control point in the specified
 * direction. 
 * <li>backspace key: reset selection region (make invisible).
 * <li>delete key: reset selection region (make invisible).
 * <li>escape key: leave selection mode (make component invisible).
 * <li>enter key: accept selection region (send off region
 * selected event) 
 * </ul>
 * Normally, the arrow keys move the cursor by 2 pixels per key
 * release; however if the shift key is pressed simultaneously,
 * then the arrow keys move the cursor by 1 pixel. 
 *
 * @author Charles Loomis
 * @version $Id: PointSelectionPanel.java,v 1.1.1.1 2004/05/31 05:08:49 kuromaru Exp $ */
public class PointSelectionPanel 
    extends GraphicalSelectionPanel {

    final private static int cursorSize=10;

    Rectangle oldBounds = new Rectangle();
    Rectangle lastDrawnRect = new Rectangle();
    Rectangle updateRect = new Rectangle();

    // Private class variables.
    private Point currentPoint = new Point();
    
    // The maximum and minimum values of the coordinates.
    private int xmin, xmax;
    private int ymin, ymax;

    // Flag to indicate whether or not the crosshair cursor is
    // visible. 
    private boolean cursorVisible;

    // This is a custom system cursor which is invisible.  Used when a
    // drag is active to avoid the system cursor from obscuring the
    // crosshairs. 
    private static Cursor invisibleCursor;

    // Temporary variable to save old cursor so that it can be
    // restored after the cursor drag has been completed. 
    private Cursor savedCursor;

    /**
     * Construct a PointSelectionPanel.  Initially the cursor is not
     * visible. */
    public PointSelectionPanel() {
        cursorVisible = false;
        setSelectionActionsEnabled(false);

        BufferedImage cursorImage = 
            new BufferedImage(16,16,BufferedImage.TYPE_INT_ARGB);
        Graphics g = cursorImage.getGraphics();
        g.setColor(new Color(0,0,0,0));
        g.fillRect(0,0,16,16);  

        Toolkit toolKit = Toolkit.getDefaultToolkit();
        invisibleCursor = toolKit.createCustomCursor(cursorImage,
                                                     new Point(0,0),
                                                     "InvisibleCursor");

        setCursor(FreeHepImage.getCursor("PointCursor"));
    }

    /**
     * Process key-released events.  This allow the selection panel to
     * move the cursor with the keyboard arrows. 
     *
     * <ul>
     * <li>arrow keys: move the active control point in the specified
     * direction. 
     * <li>backspace key: reset selection region (make invisible).
     * <li>delete key: reset selection region (make invisible).
     * <li>escape key: leave selection mode (make component invisible).
     * <li>tab key: next selection mode (next component made visible).
     * <li>enter key: accept selection region (send off region
     * selected event) 
     * <li>spacebar: accept selection region (send off region
     * selected event) 
     * </ul>
     * Normally, the arrow keys move the cursor by 2 pixels per key
     * release; however if the shift key is pressed simultaneously,
     * then the arrow keys move the cursor by 1 pixel. 
     *
     * @param e KeyEvent describing the key which has been released */
    public void keyReleased(KeyEvent e) {

        // Change the size of the increment in the given direction.
        int increment = (e.isShiftDown()) ? 1 : 2;

        switch (e.getKeyCode()) {
        case KeyEvent.VK_UP:
            updatePosition(currentPoint.x,currentPoint.y-increment);
            break;
        case KeyEvent.VK_DOWN:
            updatePosition(currentPoint.x,currentPoint.y+increment);
            break;
        case KeyEvent.VK_RIGHT:
            updatePosition(currentPoint.x+increment,currentPoint.y);
            break;
        case KeyEvent.VK_LEFT:
            updatePosition(currentPoint.x-increment,currentPoint.y);
            break;
        default:
            super.keyReleased(e);
            break;
        }
    }

    /**
     * The crosshair cursor will begin to follow the system pointer;
     * the system pointer is made invisible. 
     *
     * @param e the mouse pressed event to which to respond */
    public void mousePressed(MouseEvent e) {
        if (!isProcessingPopup(e)) {
            cursorVisible = true;
            setSelectionActionsEnabled(true);
            savedCursor = getCursor();
            setCursor(invisibleCursor);
            updatePosition(e.getX(),e.getY());
        }
    }

    /**
     * The crosshair cursor will stop following the system pointer
     * when the mouse is released.  The system pointer will again be
     * visible.
     *
     * @param e the mouse release event to which to respond */    
    public void mouseReleased(MouseEvent e) {   
        if (!isProcessingPopup(e)) {
            setCursor(savedCursor);
            updatePosition(e.getX(),e.getY());
        }
    }
    
    /**
     * The crosshair cursor follows the system pointer during a mouse
     * drag event. 
     *
     * @param e the mouse drag event to which to respond */ 
    public void mouseDragged(MouseEvent e) {
        if (!isProcessingPopup(e)) {
            updatePosition(e.getX(),e.getY());
        }
    }
    
    /**
     * This method updates the position of the crosshair cursor.
     * Normally this is called internally; however, it can be called
     * programatically by other object, for example, to move the
     * cursor on top of a selected object. 
     *
     * @param x the x coordinate for the cursor (in pixels in the
     * PointSelectionPanel's coordinate system)
     * @param y the y coordinate for the cursor (in pixels in the
     * PointSelectionPanel's coordinate system) */
    public void updatePosition(int x, int y) {

        if (cursorVisible) {

            // Get the current boundries.
            xmin = 1;
            xmax = getWidth()-1;
            ymin = 1;
            ymax = getHeight()-1;
            
            // Bring the location within bounds.
            x = Math.max(Math.min(x,xmax),xmin);
            y = Math.max(Math.min(y,ymax),ymin);        
            currentPoint.setLocation(x,y);
            
            // Update the current bounds to be the new position.  
            int xLow = currentPoint.x-cursorSize;
            int yLow = currentPoint.y-cursorSize;

            // Repaint the new bounds.
            updateRect.setBounds(lastDrawnRect);
            updateRect = SwingUtilities.computeUnion(xLow,
                                                     yLow,
                                                     2*cursorSize,
                                                     2*cursorSize,
                                                     updateRect);
            repaint(updateRect);
        }
    }

    /**
     * Set the visiblility of the crosshair cursor. 
     *
     * @param visible boolean indicating whether or not the crosshair
     * cursor should be visible */
    public void setCursorVisible(boolean visible) {
        if (cursorVisible!=visible) {
            cursorVisible = visible;
            setSelectionActionsEnabled(cursorVisible);

            repaint();
        }
    }

    /**
     * Get the visibility of the crosshair cursor.
     *
     * @return boolean indicating whether or not the crosshair cursor
     * is visible */
    public boolean getCursorVisible() {
        return cursorVisible;
    }

    /**
     * Reset the selection; this remove the crosshair cursor from the
     * screen. */
    public void resetSelection() {
        cursorVisible = false;
        updateRect.setBounds(lastDrawnRect);
        setSelectionActionsEnabled(false);
        repaint(updateRect);
    }

    public void paintComponent(Graphics g) {
        
        // Allow parent to draw any custom painting.
        super.paintComponent(g);

        if (cursorVisible) {

            // Make a 2D graphics context.
            Graphics2D g2d = (Graphics2D) g;

            // Get the limits.
            int xLow = currentPoint.x-cursorSize;
            int xHigh = currentPoint.x+cursorSize;
            int yLow = currentPoint.y-cursorSize;
            int yHigh = currentPoint.y+cursorSize;

            // Paint the crosshairs.    
            g2d.setStroke(thickStroke);
            g.setColor(Color.black);
            g.drawLine(currentPoint.x, yLow, currentPoint.x, yHigh);
            g.drawLine(xLow, currentPoint.y, xHigh, currentPoint.y);
            g2d.setStroke(thinStroke);
            g.setColor(Color.white);
            g.drawLine(currentPoint.x, yLow, currentPoint.x, yHigh);
            g.drawLine(xLow, currentPoint.y, xHigh, currentPoint.y);

            // Update the last drawn rectangle.
            lastDrawnRect.setRect(xLow-1,yLow-1,2*cursorSize+3,2*cursorSize+3);
        }
    }

    /**
     * Make the affine transform which will center the display on the
     * current point if applied.
     *
     * @return AffineTransform will center the current point */
    public AffineTransform makeAffineTransform() {
        return new AffineTransform(1.,0.,0.,1.,
                                   getWidth()/2.-currentPoint.x,
                                   getHeight()/2.-currentPoint.y);
    }

    /**
     * A utility function which creates an appropriate selection event
     * when the user accepts the current selection. */
    protected void makeSelectionEvent(int actionCode) {

        switch (actionCode) {

        case GraphicalSelectionEvent.DEFAULT_MODE: 
            resetSelection();
            setVisible(false);
            fireGraphicalSelectionMade(new 
                GraphicalSelectionEvent(this,
                                        GraphicalSelectionEvent.DEFAULT_MODE,
                                        null,null));
            break;

        case GraphicalSelectionEvent.NEXT_MODE:
            resetSelection();
            setVisible(false);
            fireGraphicalSelectionMade(new 
                GraphicalSelectionEvent(this,
                                        GraphicalSelectionEvent.NEXT_MODE,
                                        null,null));
            break;

        case GraphicalSelectionEvent.PREVIOUS_MODE:
            resetSelection();
            setVisible(false);
            fireGraphicalSelectionMade(new 
                GraphicalSelectionEvent(this,
                                        GraphicalSelectionEvent.PREVIOUS_MODE,
                                        null,null));
            break;

        case GraphicalSelectionEvent.ZOOM:
        case GraphicalSelectionEvent.ZOOM_NEW_VIEW:

            // For all of the zooming just center the selected point in the
            // view. 
            if (cursorVisible) {
                fireGraphicalSelectionMade(new 
                    PointSelectionEvent(this,actionCode,
                                        currentPoint,
                                        makeAffineTransform()));
            }
            resetSelection();
            break;

        case GraphicalSelectionEvent.PICK:
        case GraphicalSelectionEvent.PICK_ADD:
        case GraphicalSelectionEvent.UNPICK:

            // For all of the picking modify the transform to center the
            // selected point on the view, but also to zoom into the n x n
            // region around this point.
            if (cursorVisible) {

                // Get the centering transform.
                AffineTransform trans = makeAffineTransform();

                // Now get the transform which maintains the center of the
                // view but scales so that +-size pixels fill the view.
                double size = 20.;
                double halfWidth = getWidth()/2.;
                double sx = halfWidth/size;
                double halfHeight = getHeight()/2.;
                double sy = halfHeight/size;
                AffineTransform scaling = 
                    new AffineTransform(sx,0.,0.,sy,
                                        halfWidth*(1.-sx),
                                        halfHeight*(1.-sy));
                trans.preConcatenate(scaling);

                fireGraphicalSelectionMade(new 
                    PointSelectionEvent(this,actionCode,currentPoint,trans));
            }
            resetSelection();
            break;

        }
    }

}
