package ij.gui;

import java.awt.*;
import java.util.Properties;
import java.awt.image.*;
import ij.process.ImageProcessor;
import ij.measure.*;
import ij.plugin.frame.Recorder;
import ij.plugin.frame.RoiManager;
import ij.macro.*;
import ij.*;
import ij.util.Java2;
import java.awt.event.*;
import java.util.*;

/** This is a Canvas used to display images in a Window. */
public class ImageCanvas extends Canvas implements MouseListener, MouseMotionListener, Cloneable {

    protected static Cursor defaultCursor = new Cursor(Cursor.DEFAULT_CURSOR);
    protected static Cursor handCursor = new Cursor(Cursor.HAND_CURSOR);
    protected static Cursor moveCursor = new Cursor(Cursor.MOVE_CURSOR);
    protected static Cursor crosshairCursor = new Cursor(Cursor.CROSSHAIR_CURSOR);

    public static boolean usePointer = Prefs.usePointerCursor;
    
    protected ImagePlus imp;
    protected boolean imageUpdated;
    protected Rectangle srcRect;
    protected int imageWidth, imageHeight;
    protected int xMouse; // current cursor offscreen x location 
    protected int yMouse; // current cursor offscreen y location
    
    private boolean showCursorStatus = true;
    private int sx2, sy2;
    private boolean disablePopupMenu;
    private boolean showAllROIs;
    private static Color showAllColor, zoomIndicatorColor;
    private static Font smallFont, largeFont;
    private Rectangle[] labelRects;
    private boolean maxBoundsReset;
    private Vector displayList;
    private boolean labelListItems;
    private Color listColor;
    private BasicStroke listStroke;
    private static final int LIST_OFFSET = 100000;
        
    protected ImageJ ij;
    protected double magnification;
    protected int dstWidth, dstHeight;

    protected int xMouseStart;
    protected int yMouseStart;
    protected int xSrcStart;
    protected int ySrcStart;
    protected int flags;
    
    private Image offScreenImage;
    private int offScreenWidth = 0;
    private int offScreenHeight = 0;
    
    
    public ImageCanvas(ImagePlus imp) {
        this.imp = imp;
        ij = IJ.getInstance();
        int width = imp.getWidth();
        int height = imp.getHeight();
        imageWidth = width;
        imageHeight = height;
        srcRect = new Rectangle(0, 0, imageWidth, imageHeight);
        setDrawingSize(imageWidth, (int)(imageHeight));
        magnification = 1.0;
        addMouseListener(this);
        addMouseMotionListener(this);
        addKeyListener(ij);  // ImageJ handles keyboard shortcuts
        setFocusTraversalKeysEnabled(false);
    }
        
    void updateImage(ImagePlus imp) {
        this.imp = imp;
        int width = imp.getWidth();
        int height = imp.getHeight();
        imageWidth = width;
        imageHeight = height;
        srcRect = new Rectangle(0, 0, imageWidth, imageHeight);
        setDrawingSize(imageWidth, (int)imageHeight);
        magnification = 1.0;
    }

    /** Update this ImageCanvas to have the same zoom and scale settings as the one specified. */
    void update(ImageCanvas ic) {
        if (ic==null || ic==this || ic.imp==null)
            return;
        if (ic.imp.getWidth()!=imageWidth || ic.imp.getHeight()!=imageHeight)
            return;
        srcRect = new Rectangle(ic.srcRect.x, ic.srcRect.y, ic.srcRect.width, ic.srcRect.height);
        setMagnification(ic.magnification);
        setDrawingSize(ic.dstWidth, ic.dstHeight);
    }

    public void setDrawingSize(int width, int height) {
        dstWidth = width;
        dstHeight = height;
        setSize(dstWidth, dstHeight);
    }
        
    /** ImagePlus.updateAndDraw calls this method to get paint 
        to update the image from the ImageProcessor. */
    public void setImageUpdated() {
        imageUpdated = true;
    }

    public void update(Graphics g) {
        paint(g);
    }

    public void paint(Graphics g) {
        Roi roi = imp.getRoi();
        if (roi != null) {
            roi.updatePaste();
            if (Prefs.doubleBuffer && !IJ.isMacOSX())
                {paintDoubleBuffered(g); return;}
        }
        try {
            if (imageUpdated) {
                imageUpdated = false;
                imp.updateImage();
            }
            if (IJ.isJava2())
                Java2.setBilinearInterpolation(g, Prefs.interpolateScaledImages);
            Image img = imp.getImage();
            if (img!=null)
                g.drawImage(img, 0, 0, (int)(srcRect.width*magnification), (int)(srcRect.height*magnification),
                srcRect.x, srcRect.y, srcRect.x+srcRect.width, srcRect.y+srcRect.height, null);
            if (showAllROIs) showAllROIs(g);
            if (displayList!=null) drawDisplayList(g);
            if (roi != null) roi.draw(g);
            if (srcRect.width<imageWidth ||srcRect.height<imageHeight)
                drawZoomIndicator(g);
            if (IJ.debugMode) showFrameRate(g);
        }
        catch(OutOfMemoryError e) {IJ.outOfMemory("Paint");}
    }
    
    void showAllROIs(Graphics g) {
        RoiManager rm=RoiManager.getInstance();
        if (rm==null) return;
        initGraphics(g, null);
        Hashtable rois = rm.getROIs();
        java.awt.List list = rm.getList();
        int n = list.getItemCount();
        if (labelRects==null || labelRects.length!=n)
            labelRects = new Rectangle[n];
        for (int i=0; i<n; i++) {
            String label = list.getItem(i);
            Roi roi = (Roi)rois.get(label);
            if (roi==null) continue;
            drawRoi(g, roi, i);
        }
    }
    
    void drawDisplayList(Graphics g) {
        initGraphics(g, listColor);
        int n = displayList.size();
        for (int i=0; i<n; i++)
            drawRoi(g, (Roi)displayList.elementAt(i), labelListItems?i+LIST_OFFSET:-1);
        if (listStroke!=null) ((Graphics2D)g).setStroke(new BasicStroke());
    }
    
    void initGraphics(Graphics g, Color c) {
        if (showAllColor==null) {
            showAllColor = new Color(128, 255, 255);
            smallFont = new Font("SansSerif", Font.PLAIN, 9);
            largeFont = new Font("SansSerif", Font.PLAIN, 12);
        }
        if (c!=null) {
            g.setColor(c);
            if (listStroke!=null) ((Graphics2D)g).setStroke(listStroke);
        } else
        g.setColor(showAllColor);
    }
    
    void drawRoi(Graphics g, Roi roi, int index) {
        if (roi.getType()==Roi.COMPOSITE) {
            roi.setImage(imp);
            Color c = roi.getColor();
            if (index==-1 && listColor!=null)
                roi.setColor(listColor);
            else
                roi.setColor(showAllColor);
            roi.draw(g);
            roi.setColor(c);
            if (index>=0) drawRoiLabel(g, index, roi.getBounds());
        } else {
            Polygon p = roi.getPolygon();
            int x1=0, y1=0, x2=0, y2=0;
            for (int j=0; j<p.npoints; j++) {
                x2 = screenX(p.xpoints[j]);
                y2 = screenY(p.ypoints[j]);
                if (j>0) g.drawLine(x1, y1, x2, y2);
                x1=x2; y1=y2;
            }
            if (roi.isArea()&&p.npoints>0) {
                int x0 = screenX(p.xpoints[0]);
                int y0 = screenY(p.ypoints[0]);
                g.drawLine(x1, y1, x0, y0);
            }
            if (index>=0) drawRoiLabel(g, index, roi.getBounds());
        }
    }
    
    void drawRoiLabel(Graphics g, int index, Rectangle r) {
        int x = screenX(r.x);
        int y = screenY(r.y);
        double mag = getMagnification();
        int width = (int)(r.width*mag);
        int height = (int)(r.height*mag);
        int size = width>40 && height>40?12:9;
        if (size==12)
            g.setFont(largeFont);
        else
            g.setFont(smallFont);
        boolean drawingList = index >= LIST_OFFSET;
        if (drawingList) index -= LIST_OFFSET;
        String label = "" + (index+1);
        FontMetrics metrics = g.getFontMetrics();
        int w = metrics.stringWidth(label);
        x = x + width/2 - w/2;
        y = y + height/2 + Math.max(size/2,6);
        int h =  metrics.getHeight();
        g.fillRoundRect(x-1, y-h+2, w+1, h-3, 5, 5);
        if (!drawingList)
            labelRects[index] = new Rectangle(x-1, y-h+2, w+1, h-3);
        g.setColor(Color.black);
        g.drawString(label, x, y-2);
        g.setColor(showAllColor);
    } 

    void drawZoomIndicator(Graphics g) {
        int x1 = 10;
        int y1 = 10;
        double aspectRatio = (double)imageHeight/imageWidth;
        int w1 = 64;
        if (aspectRatio>1.0)
            w1 = (int)(w1/aspectRatio);
        int h1 = (int)(w1*aspectRatio);
        if (w1<4) w1 = 4;
        if (h1<4) h1 = 4;
        int w2 = (int)(w1*((double)srcRect.width/imageWidth));
        int h2 = (int)(h1*((double)srcRect.height/imageHeight));
        if (w2<1) w2 = 1;
        if (h2<1) h2 = 1;
        int x2 = (int)(w1*((double)srcRect.x/imageWidth));
        int y2 = (int)(h1*((double)srcRect.y/imageHeight));
        if (zoomIndicatorColor==null)
            zoomIndicatorColor = new Color(128, 128, 255);
        g.setColor(zoomIndicatorColor);
        g.drawRect(x1, y1, w1, h1);
        if (w2*h2<=200 || w2<10 || h2<10)
            g.fillRect(x1+x2, y1+y2, w2, h2);
        else
            g.drawRect(x1+x2, y1+y2, w2, h2);
    }

    // Use double buffer to reduce flicker when drawing complex ROIs.
    // Author: Erik Meijering
    void paintDoubleBuffered(Graphics g) {
        final int srcRectWidthMag = (int)(srcRect.width*magnification);
        final int srcRectHeightMag = (int)(srcRect.height*magnification);
        if (offScreenImage==null || offScreenWidth!=srcRectWidthMag || offScreenHeight!=srcRectHeightMag) {
            offScreenImage = createImage(srcRectWidthMag, srcRectHeightMag);
            offScreenWidth = srcRectWidthMag;
            offScreenHeight = srcRectHeightMag;
        }
        Roi roi = imp.getRoi();
        try {
            if (imageUpdated) {
                imageUpdated = false;
                imp.updateImage();
            }
            Graphics offScreenGraphics = offScreenImage.getGraphics();
            if (IJ.isJava2())
                Java2.setBilinearInterpolation(offScreenGraphics, Prefs.interpolateScaledImages);
            Image img = imp.getImage();
            if (img!=null)
                offScreenGraphics.drawImage(img, 0, 0, srcRectWidthMag, srcRectHeightMag,
                    srcRect.x, srcRect.y, srcRect.x+srcRect.width, srcRect.y+srcRect.height, null);
            if (showAllROIs) showAllROIs(offScreenGraphics);
            if (displayList!=null) drawDisplayList(offScreenGraphics);
            if (roi!=null) roi.draw(offScreenGraphics);
            if (srcRect.width<imageWidth ||srcRect.height<imageHeight)
                drawZoomIndicator(offScreenGraphics);
            if (IJ.debugMode) showFrameRate(offScreenGraphics);
            g.drawImage(offScreenImage, 0, 0, null);
        }
        catch(OutOfMemoryError e) {IJ.outOfMemory("Paint");}
    }

    long firstFrame;
    int frames, fps;
        
    void showFrameRate(Graphics g) {
        frames++;
        if (System.currentTimeMillis()>firstFrame+1000) {
            firstFrame=System.currentTimeMillis();
            fps = frames;
            frames=0;
        }
        g.setColor(Color.white);
        g.fillRect(10, 12, 50, 15);
        g.setColor(Color.black);
        g.drawString((int)(fps+0.5) + " fps", 10, 25);
    }

    public Dimension getPreferredSize() {
        return new Dimension(dstWidth, dstHeight);
    }

    int count;
    
    /*
    public Graphics getGraphics() {
        Graphics g = super.getGraphics();
        IJ.write("getGraphics: "+count++);
        if (IJ.altKeyDown())
            throw new IllegalArgumentException("");
        return g;
    }
    */

    /** Returns the current cursor location. */
    public Point getCursorLoc() {
        return new Point(xMouse, yMouse);
    }

    /** Returns the mouse event modifiers. */
    public int getModifiers() {
        return flags;
    }

    /** Sets the cursor based on the current tool and cursor location. */
    public void setCursor(int sx, int sy, int ox, int oy) {
        xMouse = ox;
        yMouse = oy;
        Roi roi = imp.getRoi();
        ImageWindow win = imp.getWindow();
        if (win==null)
            return;
        if (IJ.spaceBarDown()) {
            setCursor(handCursor);
            return;
        }
        int id = Toolbar.getToolId();
        switch (Toolbar.getToolId()) {
            case Toolbar.MAGNIFIER:
                setCursor(moveCursor);
                break;
            case Toolbar.HAND:
                setCursor(handCursor);
                break;
            default:  //selection tool
                if (id==Toolbar.SPARE1 || id>=Toolbar.SPARE2) {
                    if (Prefs.usePointerCursor) setCursor(defaultCursor); else setCursor(crosshairCursor);
                } else if (roi!=null && roi.getState()!=roi.CONSTRUCTING && roi.isHandle(sx, sy)>=0)
                    setCursor(handCursor);
                else if (Prefs.usePointerCursor || (roi!=null && roi.getState()!=roi.CONSTRUCTING && roi.contains(ox, oy)))
                    setCursor(defaultCursor);
                else
                    setCursor(crosshairCursor);
        }
    }
        
    /**Converts a screen x-coordinate to an offscreen x-coordinate.*/
    public int offScreenX(int sx) {
        return srcRect.x + (int)(sx/magnification);
    }
        
    /**Converts a screen y-coordinate to an offscreen y-coordinate.*/
    public int offScreenY(int sy) {
        return srcRect.y + (int)(sy/magnification);
    }
    
    /**Converts a screen x-coordinate to a floating-point offscreen x-coordinate.*/
    public double offScreenXD(int sx) {
        return srcRect.x + sx/magnification;
    }
        
    /**Converts a screen y-coordinate to a floating-point offscreen y-coordinate.*/
    public double offScreenYD(int sy) {
        return srcRect.y + sy/magnification;

    }
    
    /**Converts an offscreen x-coordinate to a screen x-coordinate.*/
    public int screenX(int ox) {
        return  (int)((ox-srcRect.x)*magnification);
    }
    
    /**Converts an offscreen y-coordinate to a screen y-coordinate.*/
    public int screenY(int oy) {
        return  (int)((oy-srcRect.y)*magnification);
    }

    /**Converts a floating-point offscreen x-coordinate to a screen x-coordinate.*/
    public int screenXD(double ox) {
        return  (int)((ox-srcRect.x)*magnification);
    }
    
    /**Converts a floating-point offscreen x-coordinate to a screen x-coordinate.*/
    public int screenYD(double oy) {
        return  (int)((oy-srcRect.y)*magnification);
    }

    public double getMagnification() {
        return magnification;
    }
        
    public void setMagnification(double magnification) {
        setMagnification2(magnification);
    }
        
    void setMagnification2(double magnification) {
        if (magnification>32.0) magnification = 32.0;
        this.magnification = magnification;
        imp.setTitle(imp.getTitle());
    }

    public Rectangle getSrcRect() {
        return srcRect;
    }
    
    void setSrcRect(Rectangle srcRect) {
        this.srcRect = srcRect;
    }
    
    /** Enlarge the canvas if the user enlarges the window. */
    void resizeCanvas(int width, int height) {
        ImageWindow win = imp.getWindow();
        //IJ.log("resizeCanvas: "+srcRect+" "+imageWidth+"  "+imageHeight+" "+width+"  "+height+"  "+win.maxBounds);
        if (!maxBoundsReset&& (width>dstWidth||height>dstHeight)&&win!=null&&win.maxBounds!=null&&width!=win.maxBounds.width-10) {
            // Works around problem that prevented window from being larger than maximized size
            Dimension screen = IJ.getScreenSize();
            win.setMaximizedBounds(new Rectangle(0, 0, screen.width, screen.height));
            maxBoundsReset = true;
        }
        if (srcRect.width<imageWidth || srcRect.height<imageHeight) {
            if (width>imageWidth*magnification)
                width = (int)(imageWidth*magnification);
            if (height>imageHeight*magnification)
                height = (int)(imageHeight*magnification);
            setDrawingSize(width, height);
            srcRect.width = (int)(dstWidth/magnification);
            srcRect.height = (int)(dstHeight/magnification);
            if ((srcRect.x+srcRect.width)>imageWidth)
                srcRect.x = imageWidth-srcRect.width;
            if ((srcRect.y+srcRect.height)>imageHeight)
                srcRect.y = imageHeight-srcRect.height;
            repaint();
        }
        //IJ.log("resizeCanvas2: "+srcRect+" "+dstWidth+"  "+dstHeight+" "+width+"  "+height);
    }
    
    void setMaxBounds() {
        if (maxBoundsReset) {
            maxBoundsReset = false;
            ImageWindow win = imp.getWindow();
            if (win!=null && !IJ.isLinux())
                win.setMaximizedBounds(win.maxBounds);
        }
    }

    private static final double[] zoomLevels = {
        1/72.0, 1/48.0, 1/32.0, 1/24.0, 1/16.0, 1/12.0, 
        1/8.0, 1/6.0, 1/4.0, 1/3.0, 1/2.0, 0.75, 1.0, 1.5,
        2.0, 3.0, 4.0, 6.0, 8.0, 12.0, 16.0, 24.0, 32.0 };
    
    public static double getLowerZoomLevel(double currentMag) {
        double newMag = zoomLevels[0];
        for (int i=0; i<zoomLevels.length; i++) {
        if (zoomLevels[i] < currentMag)
            newMag = zoomLevels[i];
        else
            break;
        }
        return newMag;
    }

    public static double getHigherZoomLevel(double currentMag) {
        double newMag = 32.0;
        for (int i=zoomLevels.length-1; i>=0; i--) {
            if (zoomLevels[i]>currentMag)
                newMag = zoomLevels[i];
            else
                break;
        }
        return newMag;
    }

    /** Zooms in by making the window bigger. If it can't
        be made bigger, then make the source rectangle 
        (srcRect) smaller and center it at (x,y). */
    public void zoomIn(int x, int y) {
        if (magnification>=32)
            return;
        double newMag = getHigherZoomLevel(magnification);
        int newWidth = (int)(imageWidth*newMag);
        int newHeight = (int)(imageHeight*newMag);
        Dimension newSize = canEnlarge(newWidth, newHeight);
        if (newSize!=null) {
            setDrawingSize(newSize.width, newSize.height);
            if (newSize.width!=newWidth || newSize.height!=newHeight)
                adjustSourceRect(newMag, x, y);
            else
                setMagnification(newMag);
            imp.getWindow().pack();
        } else
            adjustSourceRect(newMag, x, y);
        repaint();
    }
    
    void adjustSourceRect(double newMag, int x, int y) {
        //IJ.log("adjustSourceRect1: "+newMag+" "+dstWidth+"  "+dstHeight);
        int w = (int)Math.round(dstWidth/newMag);
        if (w*newMag<dstWidth) w++;
        int h = (int)Math.round(dstHeight/newMag);
        if (h*newMag<dstHeight) h++;
        x = offScreenX(x);
        y = offScreenY(y);
        Rectangle r = new Rectangle(x-w/2, y-h/2, w, h);
        if (r.x<0) r.x = 0;
        if (r.y<0) r.y = 0;
        if (r.x+w>imageWidth) r.x = imageWidth-w;
        if (r.y+h>imageHeight) r.y = imageHeight-h;
        srcRect = r;
        setMagnification(newMag);
        //IJ.log("adjustSourceRect2: "+srcRect+" "+dstWidth+"  "+dstHeight);
    }
    
    protected Dimension canEnlarge(int newWidth, int newHeight) {
        if ((flags&Event.SHIFT_MASK)!=0 || IJ.shiftKeyDown())
            return null;
        ImageWindow win = imp.getWindow();
        if (win==null) return null;
        Rectangle r1 = win.getBounds();

        Insets insets = win.getInsets();
        Point loc = getLocation();
        if (loc.x>insets.left+5 || loc.y>insets.top+5) {
            r1.width = newWidth+insets.left+insets.right+10;
            r1.height = newHeight+insets.top+insets.bottom+10;
            if (win instanceof StackWindow) r1.height+=20;
        } else {
            r1.width = r1.width - dstWidth + newWidth+10;
            r1.height = r1.height - dstHeight + newHeight+10;
        }
        Rectangle max = win.getMaxWindow();
        boolean fitsHorizontally = r1.x+r1.width<max.x+max.width;
        boolean fitsVertically = r1.y+r1.height<max.y+max.height;
        if (fitsHorizontally && fitsVertically)
            return new Dimension(newWidth, newHeight);
        else if (fitsVertically && newHeight<dstWidth)
            return new Dimension(dstWidth, newHeight);
        else if (fitsHorizontally && newWidth<dstHeight)
            return new Dimension(newWidth, dstHeight);
        else
            return null;
    }
        
    /**Zooms out by making the source rectangle (srcRect)  
        larger and centering it on (x,y). If we can't make it larger,  
        then make the window smaller.*/
    public void zoomOut(int x, int y) {
        if (magnification<=0.03125)
            return;
        double oldMag = magnification;
        double newMag = getLowerZoomLevel(magnification);
        double srcRatio = (double)srcRect.width/srcRect.height;
        double imageRatio = (double)imageWidth/imageHeight;
        double initialMag = imp.getWindow().getInitialMagnification();
        if (Math.abs(srcRatio-imageRatio)>0.05) {
            double scale = oldMag/newMag;
            int newSrcWidth = (int)Math.round(srcRect.width*scale);
            int newSrcHeight = (int)Math.round(srcRect.height*scale);
            if (newSrcWidth>imageWidth) newSrcWidth=imageWidth;
            if (newSrcHeight>imageHeight) newSrcHeight=imageHeight;
            int newSrcX = srcRect.x - (newSrcWidth - srcRect.width)/2;
            int newSrcY = srcRect.y - (newSrcHeight - srcRect.height)/2;
            if (newSrcX<0) newSrcX = 0;
            if (newSrcY<0) newSrcY = 0;
            srcRect = new Rectangle(newSrcX, newSrcY, newSrcWidth, newSrcHeight);
            //IJ.log(newMag+" "+srcRect+" "+dstWidth+" "+dstHeight);
            int newDstWidth = (int)(srcRect.width*newMag);
            int newDstHeight = (int)(srcRect.height*newMag);
            setMagnification(newMag);
            setMaxBounds();
            //IJ.log(newDstWidth+" "+dstWidth+" "+newDstHeight+" "+dstHeight);
            if (newDstWidth<dstWidth || newDstHeight<dstHeight) {
                //IJ.log("pack");
                setDrawingSize(newDstWidth, newDstHeight);
                imp.getWindow().pack();
            } else
                repaint();
            return;
        }
        if (imageWidth*newMag>dstWidth) {
            int w = (int)Math.round(dstWidth/newMag);
            if (w*newMag<dstWidth) w++;
            int h = (int)Math.round(dstHeight/newMag);
            if (h*newMag<dstHeight) h++;
            x = offScreenX(x);
            y = offScreenY(y);
            Rectangle r = new Rectangle(x-w/2, y-h/2, w, h);
            if (r.x<0) r.x = 0;
            if (r.y<0) r.y = 0;
            if (r.x+w>imageWidth) r.x = imageWidth-w;
            if (r.y+h>imageHeight) r.y = imageHeight-h;
            srcRect = r;
        } else {
            srcRect = new Rectangle(0, 0, imageWidth, imageHeight);
            setDrawingSize((int)(imageWidth*newMag), (int)(imageHeight*newMag));
            //setDrawingSize(dstWidth/2, dstHeight/2);
            imp.getWindow().pack();
        }
        //IJ.write(newMag + " " + srcRect.x+" "+srcRect.y+" "+srcRect.width+" "+srcRect.height+" "+dstWidth + " " + dstHeight);
        setMagnification(newMag);
        //IJ.write(srcRect.x + " " + srcRect.width + " " + dstWidth);
        setMaxBounds();
        repaint();
    }

    public void unzoom() {
        double imag = imp.getWindow().getInitialMagnification();
        if (magnification==imag)
            return;
        srcRect = new Rectangle(0, 0, imageWidth, imageHeight);
        ImageWindow win = imp.getWindow();
        setDrawingSize((int)(imageWidth*imag), (int)(imageHeight*imag));
        setMagnification(imag);
        setMaxBounds();
        win.pack();
        repaint();
    }
        
    protected void scroll(int sx, int sy) {
        int ox = xSrcStart + (int)(sx/magnification);  //convert to offscreen coordinates
        int oy = ySrcStart + (int)(sy/magnification);
        //IJ.log("scroll: "+ox+" "+oy+" "+xMouseStart+" "+yMouseStart);
        int newx = xSrcStart + (xMouseStart-ox);
        int newy = ySrcStart + (yMouseStart-oy);
        if (newx<0) newx = 0;
        if (newy<0) newy = 0;
        if ((newx+srcRect.width)>imageWidth) newx = imageWidth-srcRect.width;
        if ((newy+srcRect.height)>imageHeight) newy = imageHeight-srcRect.height;
        srcRect.x = newx;
        srcRect.y = newy;
        //IJ.log(sx+"  "+sy+"  "+newx+"  "+newy+"  "+srcRect);
        imp.draw();
        Thread.yield();
    }   
    
    Color getColor(int index){
        IndexColorModel cm = (IndexColorModel)imp.getProcessor().getColorModel();
        //IJ.write(""+index+" "+(new Color(cm.getRGB(index))));
        return new Color(cm.getRGB(index));
    }
    
    protected void setDrawingColor(int ox, int oy, boolean setBackground) {
        //IJ.write("setDrawingColor: "+setBackground+this);
        int type = imp.getType();
        int[] v = imp.getPixel(ox, oy);
        switch (type) {
            case ImagePlus.GRAY8: {
                if (setBackground)
                    setBackgroundColor(getColor(v[0]));
                else
                    setForegroundColor(getColor(v[0]));
                break;
            }
            case ImagePlus.GRAY16: case ImagePlus.GRAY32: {
                double min = imp.getProcessor().getMin();
                double max = imp.getProcessor().getMax();
                double value = (type==ImagePlus.GRAY32)?Float.intBitsToFloat(v[0]):v[0];
                int index = (int)(255.0*((value-min)/(max-min)));
                if (index<0) index = 0;
                if (index>255) index = 255;
                if (setBackground)
                    setBackgroundColor(getColor(index));
                else
                    setForegroundColor(getColor(index));
                break;
            }
            case ImagePlus.COLOR_RGB: case ImagePlus.COLOR_256: {
                Color c = new Color(v[0], v[1], v[2]);
                if (setBackground)
                    setBackgroundColor(c);
                else
                    setForegroundColor(c);
                break;
            }
        }
        Color c;
        if (setBackground)
            c = Toolbar.getBackgroundColor();
        else {
            c = Toolbar.getForegroundColor();
            imp.setColor(c);
        }
        IJ.showStatus("("+c.getRed()+", "+c.getGreen()+", "+c.getBlue()+")");
    }
    
    private void setForegroundColor(Color c) {
        Toolbar.setForegroundColor(c);
        if (Recorder.record)
            Recorder.record("setForegroundColor", c.getRed(), c.getGreen(), c.getBlue());
    }

    private void setBackgroundColor(Color c) {
        Toolbar.setBackgroundColor(c);
        if (Recorder.record)
            Recorder.record("setBackgroundColor", c.getRed(), c.getGreen(), c.getBlue());
    }

    public void mousePressed(MouseEvent e) {
        if (ij==null) return;
        showCursorStatus = true;
        int toolID = Toolbar.getToolId();
        ImageWindow win = imp.getWindow();
        if (win!=null && win.running2 && toolID!=Toolbar.MAGNIFIER) {
            win.running2 = false;
            return;
        }
        
        int x = e.getX();
        int y = e.getY();
        flags = e.getModifiers();
        //IJ.log("Mouse pressed: " + e.isPopupTrigger() + "  " + ij.modifiers(flags));      
        //if (toolID!=Toolbar.MAGNIFIER && e.isPopupTrigger()) {
        if (toolID!=Toolbar.MAGNIFIER && (e.isPopupTrigger() || (flags & Event.META_MASK)!=0)) {
            handlePopupMenu(e);
            return;
        }

        int ox = offScreenX(x);
        int oy = offScreenY(y);
        xMouse = ox; yMouse = oy;
        if (IJ.spaceBarDown()) {
            // temporarily switch to "hand" tool of space bar down
            setupScroll(ox, oy);
            return;
        }
        if (showAllROIs) {
            Roi roi = imp.getRoi();
            if (!(roi!=null && (roi.contains(ox, oy)||roi.isHandle(x, y)>=0)) && roiManagerSelect(x, y))
                return;
        }

        switch (toolID) {
            case Toolbar.MAGNIFIER:
                if (IJ.shiftKeyDown())
                    zoomToSelection(ox, oy);
                else if ((flags & (Event.ALT_MASK|Event.META_MASK|Event.CTRL_MASK))!=0)
                    zoomOut(x, y);
                else
                    zoomIn(x, y);
                break;
            case Toolbar.HAND:
                setupScroll(ox, oy);
                break;
            case Toolbar.DROPPER:
                setDrawingColor(ox, oy, IJ.altKeyDown());
                break;
            case Toolbar.WAND:
                Roi roi = imp.getRoi();
                if (roi!=null && roi.contains(ox, oy)) {
                    Rectangle r = roi.getBounds();
                    if (r.width==imageWidth && r.height==imageHeight)
                        imp.killRoi();
                    else if (!e.isAltDown()) {
                        handleRoiMouseDown(e);
                        return;
                    }
                }
                if (roi!=null) {
                    int handle = roi.isHandle(x, y);
                    if (handle>=0) {
                        roi.mouseDownInHandle(handle, x, y);
                        return;
                    }
                }
                setRoiModState(e, roi, -1);
                int npoints = IJ.doWand(ox, oy);
                if (Recorder.record && npoints>0)
                    Recorder.record("doWand", ox, oy);
                break;
            case Toolbar.OVAL:
                if (Toolbar.getBrushSize()>0)
                    new RoiBrush();
                else
                    handleRoiMouseDown(e);
                break;
            case Toolbar.SPARE1: case Toolbar.SPARE2: case Toolbar.SPARE3: 
            case Toolbar.SPARE4: case Toolbar.SPARE5: case Toolbar.SPARE6:
            case Toolbar.SPARE7: case Toolbar.SPARE8: case Toolbar.SPARE9:
                Toolbar.getInstance().runMacroTool(toolID);
                break;
            default:  //selection tool
                handleRoiMouseDown(e);
        }
    }
    
    void zoomToSelection(int x, int y) {
        IJ.setKeyUp(IJ.ALL_KEYS);
        String macro =
            "args = split(getArgument);\n"+
            "x1=parseInt(args[0]); y1=parseInt(args[1]); flags=20;\n"+
            "while (flags&20!=0) {\n"+
                "getCursorLoc(x2, y2, z, flags);\n"+
                "if (x2>=x1) x=x1; else x=x2;\n"+
                "if (y2>=y1) y=y1; else y=y2;\n"+
                "makeRectangle(x, y, abs(x2-x1), abs(y2-y1));\n"+
                "wait(10);\n"+
            "}\n"+
            "run('To Selection');\n";
        new MacroRunner(macro, x+" "+y);
    }

    boolean roiManagerSelect(int x, int y) {
        RoiManager rm=RoiManager.getInstance();
        if (rm==null) return false;
        Hashtable rois = rm.getROIs();
        java.awt.List list = rm.getList();
        int n = list.getItemCount();
        if (labelRects==null || labelRects.length!=n) return false;
        for (int i=0; i<n; i++) {
            if (labelRects[i]!=null && labelRects[i].contains(x,y)) {
                //rm.select(i);
                // this needs to run on a separate thread, at least on OS X
                new ij.macro.MacroRunner("roiManager('select', "+i+"); roiManager('update');");
                return true;
            }
        }
        return false;
    }

    protected void setupScroll(int ox, int oy) {
        xMouseStart = ox;
        yMouseStart = oy;
        xSrcStart = srcRect.x;
        ySrcStart = srcRect.y;
    }

    protected void handlePopupMenu(MouseEvent e) {
        if (disablePopupMenu) return;
        if (IJ.debugMode) IJ.log("show popup: " + (e.isPopupTrigger()?"true":"false"));
        int x = e.getX();
        int y = e.getY();
        Roi roi = imp.getRoi();
        if (roi!=null && (roi.getType()==Roi.POLYGON || roi.getType()==Roi.POLYLINE || roi.getType()==Roi.ANGLE)
        && roi.getState()==roi.CONSTRUCTING) {
            roi.handleMouseUp(x, y); // simulate double-click to finalize
            roi.handleMouseUp(x, y); // polygon or polyline selection
            return;
        }
        PopupMenu popup = Menus.getPopupMenu();
        if (popup!=null) {
            add(popup);
            if (IJ.isMacOSX()) IJ.wait(10);
            popup.show(this, x, y);
        }
    }
    
    public void mouseExited(MouseEvent e) {
        //autoScroll(e);
        ImageWindow win = imp.getWindow();
        if (win!=null)
            setCursor(defaultCursor);
        IJ.showStatus("");
    }

    /*
    public void autoScroll(MouseEvent e) {
        Roi roi = imp.getRoi();
        if (roi==null || roi.getState()!=roi.CONSTRUCTING || srcRect.width>=imageWidth || srcRect.height>=imageHeight
        || !(roi.getType()==Roi.POLYGON || roi.getType()==Roi.POLYLINE || roi.getType()==Roi.ANGLE))
            return;
        int sx = e.getX();
        int sy = e.getY();
        xMouseStart = srcRect.x+srcRect.width/2;
        yMouseStart = srcRect.y+srcRect.height/2;
        Rectangle r = roi.getBounds();
        Dimension size = getSize();
        int deltax=0, deltay=0;
        if (sx<0)
            deltax = srcRect.width/4;
        else if (sx>size.width)
            deltax = -srcRect.width/4;
        if (sy<0)
            deltay = srcRect.height/4;
        else if (sy>size.height)
            deltay = -srcRect.height/4;
        //IJ.log("autoscroll: "+sx+" "+sy+" "+deltax+" "+deltay+" "+r);
        scroll(screenX(xMouseStart+deltax), screenY(yMouseStart+deltay));
    }
    */

    public void mouseDragged(MouseEvent e) {
        int x = e.getX();
        int y = e.getY();
        xMouse = offScreenX(x);
        yMouse = offScreenY(y);
        flags = e.getModifiers();
        //IJ.log("mouseDragged: "+flags);
        if (flags==0)  // workaround for Mac OS 9 bug
            flags = InputEvent.BUTTON1_MASK;
        if (Toolbar.getToolId()==Toolbar.HAND || IJ.spaceBarDown())
            scroll(x, y);
        else {
            IJ.setInputEvent(e);
            Roi roi = imp.getRoi();
            if (roi != null)
                roi.handleMouseDrag(x, y, flags);
        }
    }

    void handleRoiMouseDown(MouseEvent e) {
        int sx = e.getX();
        int sy = e.getY();
        int ox = offScreenX(sx);
        int oy = offScreenY(sy);
        Roi roi = imp.getRoi();
        int handle = roi!=null?roi.isHandle(sx, sy):-1;     
        setRoiModState(e, roi, handle);
        if (roi!=null) {
            Rectangle r = roi.getBounds();
            int type = roi.getType();
            if (type==Roi.RECTANGLE && r.width==imp.getWidth() && r.height==imp.getHeight()
            && roi.getPasteMode()==Roi.NOT_PASTING) {
                imp.killRoi();
                return;
            }
            if (handle>=0) {
                roi.mouseDownInHandle(handle, sx, sy);
                return;
            }
            if (roi.contains(ox, oy)) {
                if (roi.modState==Roi.NO_MODS)
                    roi.handleMouseDown(sx, sy);
                else {
                    imp.killRoi();
                    imp.createNewRoi(sx,sy);
                }
                return;
            }
            if ((type==Roi.POLYGON || type==Roi.POLYLINE || type==Roi.ANGLE)
            && roi.getState()==roi.CONSTRUCTING)
                return;
            int tool = Toolbar.getToolId();
            if ((tool==Toolbar.POLYGON||tool==Toolbar.POLYLINE||tool==Toolbar.ANGLE)&& !(IJ.shiftKeyDown()||IJ.altKeyDown())) {
                imp.killRoi();
                return;
            }
        }
        imp.createNewRoi(sx,sy);
    }
    
    void setRoiModState(MouseEvent e, Roi roi, int handle) {
        if (roi==null || (handle>=0 && roi.modState==Roi.NO_MODS))
            return;
        if (roi.state==Roi.CONSTRUCTING)
            return;
        int tool = Toolbar.getToolId();
        if (tool>Toolbar.FREEROI && tool!=Toolbar.WAND && tool!=Toolbar.POINT)
            {roi.modState = Roi.NO_MODS; return;}
        if (e.isShiftDown())
            roi.modState = Roi.ADD_TO_ROI;
        else if (e.isAltDown())
            roi.modState = Roi.SUBTRACT_FROM_ROI;
        else
            roi.modState = Roi.NO_MODS;
        //IJ.log("setRoiModState: "+roi.modState+" "+ roi.state);
    }
    
    /** Disable/enable popup menu. */
    public void disablePopupMenu(boolean status) {
        disablePopupMenu = status;
    }

    public void setShowAllROIs(boolean showAllROIs) {
        this.showAllROIs = showAllROIs;
    }

    public boolean getShowAllROIs() {
        return showAllROIs;
    }
    
    public void setDisplayList(Vector list) {
        displayList = list;
        labelListItems = true;
        listColor = null;
        repaint();
    }

    public void setDisplayList(Shape shape, Color color, BasicStroke stroke) {
        Roi roi = new ShapeRoi(shape);
        Vector list = new Vector();
        list.addElement(roi);
        displayList = list;
        labelListItems = false;
        listColor = color;
        listStroke = stroke;
        repaint();
    }

    /** Called by IJ.showStatus() to prevent status bar text from
        being overwritten until the cursor moves at least 12 pixels. */
    public void setShowCursorStatus(boolean status) {
        showCursorStatus = status;
        if (status==true)
            sx2 = sy2 = -1000;
        else {
            sx2 = screenX(xMouse);
            sy2 = screenY(yMouse);
        }
    }

    public void mouseReleased(MouseEvent e) {
        flags = e.getModifiers();
        flags &= ~InputEvent.BUTTON1_MASK; // make sure button 1 bit is not set
        flags &= ~InputEvent.BUTTON2_MASK; // make sure button 2 bit is not set
        flags &= ~InputEvent.BUTTON3_MASK; // make sure button 3 bit is not set
        Roi roi = imp.getRoi();
        if (roi != null) {
            Rectangle r = roi.getBounds();
            int type = roi.getType();
            if ((r.width==0 || r.height==0)
            && !(type==Roi.POLYGON||type==Roi.POLYLINE||type==Roi.ANGLE||type==Roi.LINE)
            && !(roi instanceof TextRoi)
            && roi.getState()==roi.CONSTRUCTING
            && type!=roi.POINT)
                imp.killRoi();
            else
                roi.handleMouseUp(e.getX(), e.getY());
        }
    }

    public void mouseMoved(MouseEvent e) {
        if (ij==null) return;
        int sx = e.getX();
        int sy = e.getY();
        int ox = offScreenX(sx);
        int oy = offScreenY(sy);
        flags = e.getModifiers();
        setCursor(sx, sy, ox, oy);
        IJ.setInputEvent(e);
        Roi roi = imp.getRoi();
        if (roi!=null && (roi.getType()==Roi.POLYGON || roi.getType()==Roi.POLYLINE || roi.getType()==Roi.ANGLE) 
        && roi.getState()==roi.CONSTRUCTING) {
            PolygonRoi pRoi = (PolygonRoi)roi;
            pRoi.handleMouseMove(ox, oy);
        } else {
            if (ox<imageWidth && oy<imageHeight) {
                ImageWindow win = imp.getWindow();
                // Cursor must move at least 12 pixels before text
                // displayed using IJ.showStatus() is overwritten.
                if ((sx-sx2)*(sx-sx2)+(sy-sy2)*(sy-sy2)>144)
                    showCursorStatus = true;
                if (win!=null&&showCursorStatus) win.mouseMoved(ox, oy);
            } else
                IJ.showStatus("");

        }
    }
    
    public void mouseClicked(MouseEvent e) {}
    public void mouseEntered(MouseEvent e) {}

}