import ij.*; 
import ij.io.*; 
import ij.plugin.*; 
import ij.process.*; 
import ij.gui.*; 
import ij.plugin.frame.*; 

import java.awt.*; 
import java.awt.event.*; 
import java.awt.image.*; 


/** This plug-in provides a control dialog used to manipulate
    FluoviewTiff_ images. This control currently provides the
    following faculties:

    (1) Control whether multichannel Fluoview images are shown in
    grayscale or color.

    (2) Control which intensity mapping bounds are use to display the
    color images. Users can chose between default bounds or those
    contained in the Fluoview image.

    (3) Perform a z-projection of a Fluoview image. This is a standard
    z-projection with additional processing that ensures Fluoview
    color information is propagated to projection.

    (4) Select a set of active channels. Subsequent manipulations
    (depending on the manipulation) will only be applied to these
    active channels.

@see plugins.FluoviewTiff_
@see ij.plugin.frame.PlugInFrame

@author Patrick Kelly <phkelly@ucsd.edu> */


public class FVControl_ extends PlugInFrame 
    implements ActionListener, ItemListener {
    
    // Run as separate thread.
    Thread thread; 
    private boolean done; 
    static final int 
	NONE=0,OPEN_FILE=1,SAVE_FILE=2,
	ZPROJ=3,SET_ALL=4,SET_ACTIVE=5,
	REFRESH_STATE=6,COLOR_MODE=7,
	ROV_ADJUST=8,SPLIT_STACK=9,
	CONVERT_TO_COLOR=10; 
    private int currentOp = NONE; 

    // Manage display properties (display control)
    private Checkbox cbColorMode; 

    // Manage channel selections (channel control)
    private Button btnAllChannels; 
    private Button btnActiveChannels; 
    private List   lstChannels; 

    // Manage processing (processing control)
    private Button   btnZProj,btnDone,btnRoV,btn2Color;

    // Manage controller state 
    private Button btnRefreshState; 

    // Split stack processing
    private Button btnSplitStack; 

    // Manage i/o.
    private Button btnOpen; 
    private Button btnSave; 

    // Controller state.
    private boolean bColorMode = false; 

    // RoV adjustments
    private RoVAdjuster rovAdjuster = null; 

    // --------------------------------------------------
    public FVControl_() {
	super("Fluoview Control"); 

	buildLayout(); 
    }


    // --------------------------------------------------
    /** Implement PlugInFrame interface. */
    public void run(String arg) {
	GUI.center(this); 
	setVisible(true); 
    }


    // --------------------------------------------------
    /** Implementation of ActionListener interface. */
    public synchronized void actionPerformed(ActionEvent e) {
	// Determine what interface element was activated.
	if(e.getSource() instanceof Button)
	    processButtonEvents(e); 
    }

    // --------------------------------------------------
    /** Implementation of ItemListener interface. */
    public synchronized void itemStateChanged(ItemEvent e) {
	Checkbox cb = (Checkbox)e.getSource(); 

	if(cb==cbColorMode)
	    currentOp = COLOR_MODE; 

	//notify(); 
	doUpdate(); 
    }

    // --------------------------------------------------
    /** Override parent class handler to perform exit processing. */
    public void processWindowEvent(WindowEvent e) {
	super.processWindowEvent(e); 
    }

    // --------------------------------------------------
    public boolean colorMode() {
	return bColorMode; 
    }

    // --------------------------------------------------
    protected void buildLayout() {
	// Place entire layout in GridBag
	GridBagLayout gbl = new GridBagLayout(); 
	GridBagConstraints gbc = new GridBagConstraints(); 
	setLayout(gbl); 

	// Build (nested) control panels
	Panel pDC = displayControl(); 
	Panel pCC = channelControl(); 
	Panel pB  = controlButtons(); 

	// Layout all control panels
	gbc.anchor = GridBagConstraints.NORTHWEST; 
	gbc.fill   = GridBagConstraints.NONE; 
	gbc.gridwidth = GridBagConstraints.REMAINDER; 
	gbl.setConstraints(pDC,gbc); 
	add(pDC); 

	gbl.setConstraints(pCC,gbc); 
	add(pCC); 

	gbl.setConstraints(pB,gbc); 
	add(pB); 

	setResizable(false); 

	pack(); 
    }

    // --------------------------------------------------
    /** Build a panel that supports control of the color display. */
    protected Panel displayControl() {
	Panel p = new Panel(); 
	cbColorMode = new 
	    Checkbox("View multichannel (color) display",false); 
	cbColorMode.addItemListener(this); 

	p.add(cbColorMode); 

	return p; 
    }


    // --------------------------------------------------

    /** Build a panel that supports selection of active channels. */
    protected Panel channelControl() {
	Panel p = new Panel(); 
	// Gridbag to layout entire display.
	GridBagLayout gbl = new GridBagLayout(); 
	GridBagConstraints gbc = new GridBagConstraints(); 
	p.setLayout(gbl); 
	
	// Layout channel control buttons in grid layout.
	Panel pselect = new Panel(new GridLayout(2,1)); 

	// Select all channels or active channels.
	btnAllChannels     = new Button("Use All Channels"); 
	pselect.add(btnAllChannels); 
	btnActiveChannels  = new Button("Use Active Channels"); 
	pselect.add(btnActiveChannels); 

	btnAllChannels.addActionListener(this); 
	btnActiveChannels.addActionListener(this); 

	// Layout choice of channels in separate panel.
	Panel plist = new Panel(new BorderLayout()); 
	Label l = new Label("Channels"); 
	plist.add(l,"North"); 
	lstChannels = new List(3); 
	lstChannels.setMultipleMode(true); 
	
	setChannelSelections(lstChannels,0); 
	plist.add(lstChannels,"Center"); 

	// Layout entire panel.
	gbc.anchor = GridBagConstraints.NORTHWEST; 
	gbc.fill   = GridBagConstraints.BOTH; 
	gbc.gridheight = GridBagConstraints.REMAINDER; 
	gbc.insets = new Insets(5,5,5,5); 
	gbl.setConstraints(plist,gbc); 
	p.add(plist); 

	gbc.gridwidth = GridBagConstraints.REMAINDER; 
	gbl.setConstraints(pselect,gbc); 
	p.add(pselect); 

	return p; 
    }

    // --------------------------------------------------
    protected Panel controlButtons() {
	Panel p = new Panel(); 
	p.setLayout(new GridLayout(3,3,5/*hgap*/,5/*vgap*/)); 

	// Processing buttons
	btnZProj = new Button("Z Projection"); 
	btnRoV   = new Button("Adjust Range of Values"); 
	btnRefreshState = new Button("Refresh Controller"); 

	btnZProj.addActionListener(this); 
	btnRoV.addActionListener(this); 
	btnRefreshState.addActionListener(this); 

	p.add(btnZProj); p.add(btnRoV); p.add(btnRefreshState); 

	// Split stack processing
	btnSplitStack = new Button("Split FV Stack"); 
	btnSplitStack.addActionListener(this); 
	p.add(btnSplitStack); 

	// I/O buttons
	btnOpen = new Button("Open Fluoview Image"); 
	btnSave = new Button("Save"); 

	// Add action listeners
	btnOpen.addActionListener(this); 
	btnSave.addActionListener(this); 

	p.add(btnOpen); p.add(btnSave); 

	// Additional processing
	btn2Color = new Button("Convert to RGB"); 

	btn2Color.addActionListener(this); 

	p.add(btn2Color); 

	return p; 
    }

    
    // --------------------------------------------------
    /** */
    protected void colorDisplayModeOn(FluoviewTiff_ fvimp){
	// Indicate that image display should use color display mode
	// and redraw.
	fvimp.setComposite(true,true); 

	bColorMode = true; 
    }

    // --------------------------------------------------
    protected void colorDisplayModeOff(FluoviewTiff_ fvimp){
	// Indicate that image display should use standard gray scale
	// display mode and redraw.
	fvimp.setComposite(false,true); 

	bColorMode = false; 
    }


    // --------------------------------------------------
    protected void setChannelSelections(List lchannels, 
					int numChannels) {
	// Clear any current channel information
	lchannels.removeAll(); 

	if(numChannels==0) {
	    String chname = "No channels";
	    lchannels.add(chname); 
	}
	else {
	    for(int n=0;n<numChannels;++n) {
		String chname = "Channel "+Integer.toString(n); 
		lchannels.add(chname); 
	    }
	}
    }

    // --------------------------------------------------
    private void processButtonEvents(ActionEvent e) {
	Button bpressed = (Button)e.getSource(); 
	if(bpressed==btnOpen)
	    currentOp = OPEN_FILE; 
	else if(bpressed==btnSave)
	    currentOp = SAVE_FILE; 
	else if(bpressed==btnSplitStack)
	    currentOp = SPLIT_STACK; 
	else if(bpressed==btnZProj)
	    currentOp = ZPROJ; 
	else if(bpressed==btnAllChannels)
	    currentOp = SET_ALL; 
	else if(bpressed==btnActiveChannels)
	    currentOp = SET_ACTIVE; 
	else if(bpressed==btnRefreshState)
	    currentOp = REFRESH_STATE; 
	else if(bpressed==btnRoV)
	    currentOp = ROV_ADJUST; 
	else if(bpressed==btn2Color)
	    currentOp = CONVERT_TO_COLOR; 

	// Wake up associate thread to process operation.
	//notify(); 
	doUpdate(); 
    }

    // --------------------------------------------------
    private void doOpen() {
	FluoviewTiff_ fvimp = new FluoviewTiff_();
	fvimp.run(""); 

	// TODO - Handle case where file dialog is caneled. In this
	// case we don't want to execute following reset/refresh state
	// calls. If canceled, ImageWindow would not be initilized. So
	// make sure this is not null.
	ImageWindow win = fvimp.getWindow(); 
	if(win==null) return; 
	

	// Refresh controller internal state.
	refreshState(fvimp); 

	// Make sure image color display mode is on.
	cbColorMode.setState(true); 
	colorDisplayModeOn(fvimp); 
    }


    // --------------------------------------------------
    private void doSave(FluoviewTiff_ fvimp) {
	SaveExTiff_ svet = new SaveExTiff_(); 
	svet.saveImage(fvimp); 
    }


    // --------------------------------------------------
    private FluoviewTiff_ getImage() {
	ImagePlus imp = WindowManager.getCurrentImage(); 

	// Null images are valid.
	if(imp==null) return null; 

	// Not null so make sure active image is valid Fluoview type.
	if(imp!=null) {
	    if(FluoviewTiff_.isFluoviewImage(imp)==false)
		return null; 
	}

	// Not null and valid so refresh image object.
	return (FluoviewTiff_)imp; 
    }

    // --------------------------------------------------
    private void doZProjection(FluoviewTiff_ fvimp) {
	// Build dialog to get projection parameters.
	GenericDialog gd = new GenericDialog("ZProjection",
					     IJ.getInstance()); 
	gd.addNumericField("Start slice:",1,0/*digits*/); 
	gd.addNumericField("Stop slice:",fvimp.getStackSize(),0/*digits*/); 
	// Choice of channels.
	String[] schannels =  lstChannels.getItems(); 
	gd.addChoice("Use LUT from Channel", schannels,schannels[0]); 

	gd.showDialog();
	if(gd.wasCanceled()) return; 
	
	// Set up projection parameters using user specified values
	// and choices.
	ZProjector zproj = new ZProjector(fvimp); 
	zproj.setImage(fvimp); 

	int sslice = (int)gd.getNextNumber(); 
	int eslice = (int)gd.getNextNumber(); 

	zproj.setStartSlice(sslice); 
	zproj.setStopSlice(eslice); 

	int nchannel = gd.getNextChoiceIndex(); 
	//String smethod = gd.getNextChoice(); 

	// Compute projection using maximum intensity projection.
	zproj.setMethod(ZProjector.MAX_METHOD); 
	zproj.doProjection();

	// Output z-projection image should be 16-bit image.  So all
	// we have to do is convert to FluoviewTiff_ type and set
	// channel/acquisition parameters.
	ImagePlus impProj = zproj.getProjection(); 
	ImageProcessor sp = impProj.getProcessor(); 
	if(!(sp instanceof ShortProcessor)) {
	    IJ.error("Z-Projection is not short. Can not convert non-short to FluoviewTiff"); 
	    return; 
	}

	// Create new FluoviewTiff_ image using z-projection image processor.
	FluoviewTiff_ fvproj = new FluoviewTiff_(impProj.getTitle(),sp); 

	// Set channel properties based on user specified channel.
	FluoviewTiff_.AcqParams ap = fvimp.getAcqParams(); 
	FluoviewTiff_.CProps cp = fvimp.getChannelProps(nchannel); 
	fvproj.setChannelProps(cp);

	// Set acquistion parameters. Same for all channels.
	fvproj.setAcqParams(ap); 

	fvproj.initImageDescription(); 

	// Propagate scale information.
	fvproj.copyScale(fvimp); 

	// Update image w/ processor.
	sp.setMinAndMax(0.0,0.0);   // autoscaling

	fvproj.show(); 

	fvproj.setComposite(fvimp.isFVComposite(),true); 

	// Projected image is now active so refresh state to reflect
	// its parameters.
	resetState(); 
	refreshState(fvproj); 
    }

    // --------------------------------------------------
    private void setAllChannels(FluoviewTiff_ fvimp) {
	// Make sure number of channels on list is consistent w/
	// number in input image.
	if(lstChannels.getItemCount()!= fvimp.getNumberChannels()) {
	    IJ.error("Controller state not consistent w/ input image"+
		     "\nRefresh controller state"); 
	    return; 
	}
	// Indicate that all channels are selected.
	for(int n=0;n<lstChannels.getItemCount();++n) {
	    lstChannels.select(n); 
	    fvimp.activateChannel(n); 
	}

	// Redraw 
	fvimp.updateAndRepaintWindow(); 
    }


    // --------------------------------------------------
    private void setActiveChannels(FluoviewTiff_ fvimp) {
	// Make sure number of channels on list is consistent w/
	// number in input image.
	if(lstChannels.getItemCount()!=fvimp.getNumberChannels()) {
	    IJ.error("Controller state not consistent w/ input image"+
		     "\nUpdate controller state"); 
	    return; 
	}

	for(int n=0;n<lstChannels.getItemCount();++n) {
	    if(lstChannels.isIndexSelected(n))
		fvimp.activateChannel(n);
	    else
		fvimp.inactivateChannel(n); 
	}
	
	// Redraw 
	fvimp.updateAndRepaintWindow(); 
    }

    // --------------------------------------------------
    private void refreshState(FluoviewTiff_ fvimp) {
	// Deal w/ null input image.
	if(fvimp==null) {
	    setChannelSelections(lstChannels,0); 
	    setColorMode(false); 
	    return; 
	}
	// Read state from input image and set controller accordingly.
	int numChannels = 0; 
	if(fvimp!=null)
	    numChannels = fvimp.getNumberChannels();
	
	setChannelSelections(lstChannels,numChannels); 

	// Select active channels.
	for(int n=0;n<fvimp.getNumberChannels();++n) {
	    if(fvimp.isActive(n))
		lstChannels.select(n); 
	    else 
		lstChannels.deselect(n); 
	}

	// Set color display mode indicator to be consistent w/ input
	// image.
	setColorMode(fvimp.isFVComposite()); 
	
	// Refresh adjuster if it is displayed.
	if(rovAdjuster!=null)
	    rovAdjuster.refresh(fvimp); 
    }

    // --------------------------------------------------
    private void setColorMode(boolean b) {
	cbColorMode.setState(b);
	bColorMode = b; 
    }


    // --------------------------------------------------
    private void resetState() {
	setColorMode(true); 
    }

    // --------------------------------------------------
    private void doUpdate() {
	// Special case for file open, does not rely on a valid
	// FluoviewTiff_ image object. So handle this first.
	if(currentOp==OPEN_FILE) {
	    doOpen();
	    currentOp = NONE; 
	    return;
	}

	// Other operations rely on valid FluoviewTiff_ image object.
	// So first get current image and make sure it is Fluoview
	// type. If op is controller refresh, than a null image is ok.
	FluoviewTiff_ fvimp = getImage(); 
	if(fvimp==null && currentOp!=REFRESH_STATE) {
	    IJ.error("Valid image not selected."); 
	    return; 
	}

	// Have valid FluoviewTiff_ image object so do the operation.
	switch(currentOp) {
	case SAVE_FILE:
	    doSave(fvimp); break; 
	case ZPROJ:
	    doZProjection(fvimp); break; 
	case SET_ALL:
	    setAllChannels(fvimp); break; 
	case SET_ACTIVE:
	    setActiveChannels(fvimp); break; 
	case REFRESH_STATE:
	    refreshState(fvimp); break; 
	case COLOR_MODE:
	    colorDisplayModeUpdate(fvimp); break; 
	case ROV_ADJUST: 
	    initRovAdjuster(fvimp); break; 
	case SPLIT_STACK:
	    splitStack(fvimp); break; 
	case CONVERT_TO_COLOR:
	    convert2Color(fvimp); break; 
	}; 

	currentOp = NONE; 
    }

    // --------------------------------------------------
    private void colorDisplayModeUpdate(FluoviewTiff_ fvimp) {
	if(cbColorMode.getState()==true)
	    colorDisplayModeOn(fvimp); 
	else
	    colorDisplayModeOff(fvimp); 

	fvimp.updateAndRepaintWindow(); 
    }

    // --------------------------------------------------
    private void splitStack(FluoviewTiff_ fvimp) {
	FVSplitStack_ fvss = new FVSplitStack_(); 
	fvss.splitStack(fvimp); 
    }

    // --------------------------------------------------
    private void convert2Color(FluoviewTiff_ fvimp) {
	FVToColor_ fvToColor = new FVToColor_(); 

	fvToColor.convert(fvimp); 
    }

    // --------------------------------------------------
    private void initRovAdjuster(FluoviewTiff_ fvimp) {
	// Initialize on first use.
	if(rovAdjuster==null)
	    rovAdjuster = new RoVAdjuster(this); 
	
	rovAdjuster.setVisible(true); 

	// Set RoVAdjuster up to use current image.
	rovAdjuster.setup(fvimp); 
	rovAdjuster.refresh(fvimp); 

    }

    // --------------------------------------------------
    public void killAdjuster() {
	rovAdjuster = null; 
    }

}  // FVControl_


// --------------------------------------------------
/** RoVAdjuster provides a graphical mechanism to set the Range of
Values (RoV) used to render an image. It is based on the ImageJ
ij.plugin.frame.ContrastAdjuster class. It is multithreaded to provide
a more responsive user interface. */
class RoVAdjuster 
    extends PlugInFrame 
    implements PlugIn, Runnable, ActionListener, AdjustmentListener, 
	       ItemListener,MouseListener, MouseMotionListener
{
    public static final String REFRESH_STATE = "Refresh"; 
    public static final String RESET_IMAGE  = "Reset"; 
    public static final String RESET_ALL    = "Reset All"; 
    public static final String HISTOGRAM    = "Hist."; 
    public static final String APPLY        = "Apply"; 

    public static final int INTENSITY_MIN = 0; 
    public static final int INTENSITY_MAX = 4095; 

    public static final int MAX_CHANNELS = 5; 

    private static final int SCROLL_SIZE = 100; 
    private static final int SCROLL_VIS  = 1; 
    private static final double SCALE = 
	(double)INTENSITY_MAX/SCROLL_SIZE; 

    private int SIZE = 0; 

    ImageJ ij; 

    private RoVPlot rovPlot;
    private Thread thread; 

    int minRoV = -1; 
    int maxRoV = -1; 

    boolean doRefresh = false; 
    boolean doReset = false;
    boolean doResetAll = false; 
    boolean doHistogram = false;
    boolean doApplyRoV = false;
    boolean doMaxUpdate = false; 
    boolean doMinUpdate = false; 
    boolean doUpdateChannel = false; 
    boolean doPlotUpdate    = false; 
	
    Panel panel, tPanel;
    int previousImageID;

    Scrollbar sldrMinRoV,sldrMaxRoV; 
    TextField txtMinRoV,txtMaxRoV; 

    Label minLabel,maxLabel,lblMinRoV,lblMaxRoV; 

    // Chose which channel to adjust.
    Choice chChannels; 

    // Currently active channel.
    int nCurrentChannel = 0;  
    int nDefaultChannel = 0; 

    boolean done = false;

    private boolean bColorDisplay = false; 

    private FVControl_ fvctrl; 

    // --------------------------------------------------
    //
    public RoVAdjuster(FVControl_ fvctrl) {
	super("RoV Adjuster");
	ij = IJ.getInstance();

	this.fvctrl = fvctrl; 

	buildRoVPlot(); 

	buildLayout(); 

	GUI.center(this);
	setVisible(true);

	thread = new Thread(this, "RoV Adjuster");
	thread.start();
    }

    // --------------------------------------------------
    /** Terminate thread. */
    public synchronized void finished() {
	fvctrl.killAdjuster(); 
	done = true; 

    }

    // --------------------------------------------------
    public synchronized void itemStateChanged(ItemEvent e) {
	Choice ch = (Choice)e.getSource(); 
	// Get selected index and update current channel choice.
	nCurrentChannel = ch.getSelectedIndex(); 

	// Update histogram w/ new channel choice.
	doUpdateChannel = true; 

	notify(); 
	//doUpdate(); 
    }


    // --------------------------------------------------
    public synchronized void adjustmentValueChanged(AdjustmentEvent e) {
	if (e.getSource()==sldrMaxRoV) {
	    maxRoV = (int)(sldrMaxRoV.getValue()*SCALE);
	    doMaxUpdate = true; 
	}
	else {
	    minRoV = (int)(sldrMinRoV.getValue()*SCALE);
	    doMinUpdate = true; 
	}
	notify();
	//doUpdate(); 
    }

    // --------------------------------------------------
    //
    // Separate thread that does the potentially time-consuming
    // processing
    public void run() {
	while (!done) {
	    synchronized(this) {
		try {wait();}
		catch(InterruptedException e) {}
	    }
	    doUpdate();
	}
    }

    // --------------------------------------------------
    public synchronized  void actionPerformed(ActionEvent e) {
	String label = e.getActionCommand();
	if (label==null)
	    return;
	if (label.equals(RESET_IMAGE))
	    doReset = true;
	else if (label.equals(RESET_ALL))
	    doResetAll = true; 
	else if (label.equals(REFRESH_STATE))
	    doRefresh = true;
	else if (label.equals(HISTOGRAM))
	    doHistogram = true;
	else if (label.equals(APPLY))
	    doApplyRoV = true;

	notify();
	//doUpdate(); 
    }

    // --------------------------------------------------
    public synchronized void plotChange(int min, int max) {
	minRoV = min; 
	maxRoV = max; 

	doPlotUpdate = true; 

	notify(); 
	//doUpdate(); 
    }


    // --------------------------------------------------
    //
    // Encode actions rov adjuster can respond to.
    static final int RESET_ACTION=0,REFRESH_ACTION=1,HIST_ACTION=2, 
	APPLY_ACTION=3,CHANGE_MIN_ACTION=4,CHANGE_MAX_ACTION=5,
	CHANNEL_ACTION=6,PLOT_UPDATE_ACTION=7,RESET_ALL_ACTION=8; 


    // --------------------------------------------------
    private void doUpdate() {
	if(colorMode()==false) {
	    IJ.error("Must be in color display mode to perform"
		     +" RoV adjustments"); 
	    return; 
	}

	// Retrieve currently active image. Make sure it is a Fluoview
	// image.
	FluoviewTiff_ fvimp = getValidImage(); 
	if(fvimp==null) return; 

	// Set up adjuster state w/ input image.
	setup(fvimp);

	// Specify action branch based on user input.
	int action; 
	if (doReset)         action = RESET_ACTION;
	else if(doResetAll)  action = RESET_ALL_ACTION; 
	else if(doRefresh)   action = REFRESH_ACTION;
	else if(doHistogram) action = HIST_ACTION;
	else if(doApplyRoV)  action = APPLY_ACTION;
	else if(doMinUpdate) action = CHANGE_MIN_ACTION;
	else if(doMaxUpdate) action = CHANGE_MAX_ACTION; 
	else if(doUpdateChannel) action = CHANNEL_ACTION; 
	else if(doPlotUpdate)    action = PLOT_UPDATE_ACTION; 
	else return;

	// Reset state
	doReset         = false; 
	doResetAll      = false; 
	doRefresh       = false; 
	doHistogram     = false; 
	doApplyRoV      = false; 
	doMinUpdate     = false; 
	doMaxUpdate     = false; 
	doUpdateChannel = false; 
	doPlotUpdate    = false; 

	// Make sure we can access image.
	if (!fvimp.lock()) {
	    IJ.beep();
	    IJ.showStatus("Could not get lock."); 
	    fvimp=null; 
	    return;
	}

	// Perform the action.
	switch(action) {
	case RESET_ACTION:      reset(fvimp);         break; 
	case RESET_ALL_ACTION:  resetAll(fvimp);      break; 
	case REFRESH_ACTION:    refresh(fvimp);       break; 
	case HIST_ACTION:       plotHistogram(fvimp); break; 
	case APPLY_ACTION:      apply(fvimp);         break; 
	case CHANGE_MIN_ACTION: updateMin(fvimp);     break; 
	case CHANGE_MAX_ACTION: updateMax(fvimp);     break; 
	case CHANNEL_ACTION:    updateChannel(fvimp); break; 
	case PLOT_UPDATE_ACTION: plotUpdate(fvimp);   break; 
	}

	updatePlot();
	updateLabels();
	// Redraw image so that changes are displayed.
	fvimp.updateAndDraw();

	// Let other processes access image.
	fvimp.unlock();

    }

    // --------------------------------------------------
    private void plotHistogram(FluoviewTiff_ fvimp) {
	rovPlot.setHistogram(fvimp.getStatistics(nCurrentChannel)); 
    }

    // --------------------------------------------------
    /** Retrieves current image and does a series of checks to make
	sure it is valid, i.e. it can be processed by the RoV
	adjustment routines.  */
    private FluoviewTiff_ getValidImage() {
	ImagePlus imp = WindowManager.getCurrentImage();
	if (imp==null) {
	    IJ.beep(); IJ.showStatus("No image");
	    return null;
	}

	// Image not null so check that it is FluoviewTiff_ type.
	if(FluoviewTiff_.isFluoviewImage(imp)==false) {
	    IJ.beep(); 
	    IJ.showStatus("Can only adjust RoV for FluoviewTiff_ images."); 
	    return null; 
	}

	// At this point so have a valid FluoviewTiff_ image.  Set
	// adjuster state based on this image.
	FluoviewTiff_ fvimp = (FluoviewTiff_)imp; 

	// Make sure this image is consistent w/ current channel
	// parameters.
	if(fvimp.getNumberChannels() <= nCurrentChannel) {
	    // Reset number of channels.
	    nCurrentChannel = 0; 
	}

	return fvimp; 
    }

    // --------------------------------------------------
    private void updateLabels() {
	minLabel.setText(""+minRoV); 
	maxLabel.setText(""+maxRoV); 
    }


    // --------------------------------------------------
    /** Returns boolean indicating whether or not associated
	FVControl_ object is current in color display mode on
	state. */
    private boolean colorMode() {
	return fvctrl.colorMode(); 
    }

    // --------------------------------------------------
    /** Set up state of RoV adjuster based on parameters in input image */
    void setup(FluoviewTiff_ fvimp) {
	// Read intensity mapping parameters.

	if(fvimp.getID()!=previousImageID) {
	    // This is new image so make sure changes to its 
	    // channel properties can be undone.
	    //fvimp.initChannelUndo(); 

	    // Set up min/max RoVs.
	    FluoviewTiff_.CProps cp = 
		fvimp.getChannelProps(nCurrentChannel); 

	    minRoV = cp.getMin(); 
	    maxRoV = cp.getMax(); 

	    // Update plotting parameters w/ new image info.
	    //rovPlot.histogram = null;
	    // Initialize histogram w/ new image data.
	    rovPlot.setHistogram(fvimp.getStatistics(nCurrentChannel)); 
	}

	previousImageID = fvimp.getID();
    }

    // --------------------------------------------------
    private void reset(FluoviewTiff_ fvimp) {
	fvimp.resetChannelProps(nCurrentChannel); 

	FluoviewTiff_.CProps cp = 
	    fvimp.getChannelProps(nCurrentChannel); 

	minRoV = cp.getMin(); 
	maxRoV = cp.getMax(); 

	// Update controller state.
	updateSliders(); 
    }

    // --------------------------------------------------
    private void resetAll(FluoviewTiff_ fvimp) {
	fvimp.resetChannelProps(); 

	FluoviewTiff_.CProps cp = 
	    fvimp.getChannelProps(nCurrentChannel); 

	minRoV = cp.getMin(); 
	maxRoV = cp.getMax(); 

	// Update controller state.
	updateSliders(); 
    }

    // --------------------------------------------------
    private void apply(FluoviewTiff_ fvimp) {
	// Update default values in input image.
	fvimp.initChannelUndo(); 

	for(int n=0; n<fvimp.getNumberChannels();++n) {
	    FluoviewTiff_.CProps cp = fvimp.getChannelProps(n); 
	}

	// Build image description to reflect new parameters.
	fvimp.initImageDescription(); 
    }


    // --------------------------------------------------
    private void buildRoVPlot() {
	rovPlot = new RoVPlot();
	rovPlot.addMouseListener(this); 
	rovPlot.addMouseMotionListener(this); 

	// Get size for later use
	Dimension sz = rovPlot.getSize(); 
	SIZE = sz.width; 
    }

    // --------------------------------------------------
    private void buildLayout() {
	Font monoFont = new Font("Monospaced", Font.PLAIN, 12);
	Font sanFont = new Font("SansSerif", Font.PLAIN, 12);
	
	GridBagLayout gridbag = new GridBagLayout();
	GridBagConstraints c = new GridBagConstraints();
	setLayout(gridbag);
		
	// plot is at grid cell (0,0)
	c.gridx = 0;
	int y = 0;	
	c.gridy = y++;
	c.fill = GridBagConstraints.BOTH;
	c.anchor = GridBagConstraints.CENTER;
	c.insets = new Insets(10, 10, 0, 10);
	gridbag.setConstraints(rovPlot, c);
	add(rovPlot);
		
	// min and max
	panel = new Panel();
	c.gridy = y++;
	c.insets = new Insets(2, 10, 0, 10);
	gridbag.setConstraints(panel, c);
    	panel.setLayout(new BorderLayout());
	minLabel = new Label("       ", Label.LEFT);
    	minLabel.setFont(monoFont);
	panel.add("West", minLabel);
	maxLabel = new Label("     ", Label.RIGHT);
    	maxLabel.setFont(monoFont);
	panel.add("East", maxLabel);
	add(panel);

	// Channel choice.
	panel = new Panel(new GridLayout(2,1));
	Label lbl = new Label("Channels"); 
	panel.add(lbl); 
	chChannels = new Choice(); 
	chChannels.addItem("No channels yet"); 
	panel.add(chChannels); 

	chChannels.addItemListener(this); 

	c.gridy = y++; 
	c.insets = new Insets(10,10,0,10); 
	gridbag.setConstraints(panel,c); 
	add(panel); 

	// Slider for minimum RoV
	sldrMinRoV = new Scrollbar(Scrollbar.HORIZONTAL, 
				   SCROLL_SIZE/2, 
				   SCROLL_VIS, 
				   0, SCROLL_SIZE);
	c.gridy = y++;
	c.insets = new Insets(8, 10, 0, 10);
	gridbag.setConstraints(sldrMinRoV,c); 
	add(sldrMinRoV); 
	
	sldrMinRoV.addAdjustmentListener(this);
	sldrMinRoV.setUnitIncrement(2);
		
	// Minimum RoV label
	panel = new Panel();
	c.gridy = y++;
	c.insets = new Insets(2, 10, 0, 10);
	gridbag.setConstraints(panel, c);
    	panel.setLayout(new FlowLayout(FlowLayout.CENTER, 0, 0));
    	Label label = new Label("Minimum Value");
    	label.setFont(sanFont);
	panel.add(label);
	lblMinRoV = new Label("", Label.LEFT);
    	lblMinRoV.setFont(monoFont);
	panel.add(lblMinRoV);
	add(panel);

	// Maximum RoV slider
	sldrMaxRoV = new Scrollbar(Scrollbar.HORIZONTAL, 
				   SCROLL_SIZE/2, 
				   SCROLL_VIS, 
				   0, SCROLL_SIZE);
	c.gridy = y++;
	c.insets = new Insets(8, 10, 0, 10);
	gridbag.setConstraints(sldrMaxRoV,c); 
	add(sldrMaxRoV); 

	sldrMaxRoV.addAdjustmentListener(this);
	sldrMaxRoV.setUnitIncrement(2);
		
	// Max. RoV label
	panel = new Panel();
	c.gridy = y++;
	c.insets = new Insets(2, 10, 0, 10);
	gridbag.setConstraints(panel, c);
    	panel.setLayout(new FlowLayout(FlowLayout.CENTER, 0, 0));
    	label = new Label("Maximum Value");
    	label.setFont(sanFont);
	panel.add(label);
	lblMaxRoV = new Label("", Label.LEFT);
    	lblMaxRoV.setFont(monoFont);
	panel.add(lblMaxRoV);
	add(panel);

	// buttons
	panel = new Panel();
	panel.setLayout(new GridLayout(3, 2, 5, 5));

	Button b = new Button(REFRESH_STATE); 
	b.addActionListener(this);
	b.addKeyListener(ij);
	panel.add(b);

	b = new Button(RESET_IMAGE); 
	b.addActionListener(this);
	b.addKeyListener(ij);
	panel.add(b);

	b = new Button(HISTOGRAM); 
	b.addActionListener(this);
	b.addKeyListener(ij);
	panel.add(b);

	b = new Button(RESET_ALL); 
	b.addActionListener(this); 
	b.addKeyListener(ij); 
	panel.add(b); 

	b = new Button(APPLY); 
	b.addActionListener(this);
	b.addKeyListener(ij);
	panel.add(b);
	c.gridy = y++;
	c.insets = new Insets(10, 5, 10, 5);
	gridbag.setConstraints(panel, c);
	add(panel);
		
	addKeyListener(ij);  // ImageJ handles keyboard shortcuts
	pack();
    }

    // --------------------------------------------------
    private Panel layoutSlider(Scrollbar sb, TextField tf) {
	Panel p = new Panel(); 
	GridBagLayout gbl = new GridBagLayout(); 
	GridBagConstraints gbc = new GridBagConstraints(); 
	p.setLayout(gbl); 

	gbc.fill = GridBagConstraints.BOTH;
	gbc.anchor = GridBagConstraints.NORTHWEST; 
	gbc.insets = new Insets(5,5,5,5); 
	gbl.setConstraints(sb,gbc); 
	p.add(sb); 

	return p; 
    }

    // --------------------------------------------------
    public void updatePlot() {
	rovPlot.minRoV = minRoV; 
	rovPlot.maxRoV = maxRoV; 
	rovPlot.repaint(); 
    }


    // --------------------------------------------------
    private void updateMinSlider() {
	double dmin = ((double)minRoV/INTENSITY_MAX)*SCROLL_SIZE; 
	sldrMinRoV.setValue((int)dmin); 
    }

    // --------------------------------------------------
    private void updateMaxSlider() {
	double dmax = ((double)maxRoV/INTENSITY_MAX)*SCROLL_SIZE; 
	sldrMaxRoV.setValue((int)dmax); 
    }

    // --------------------------------------------------
    private void updateSliders() {
	updateMinSlider();
	updateMaxSlider(); 
    }

    // --------------------------------------------------
    public void refresh(FluoviewTiff_ fvimp) {
	// Clear current channel choices.
	chChannels.removeAll(); 

	// Update choice display, scroll bars and histogram w/ input
	// image parameters.
	int nc = fvimp.getNumberChannels();

	buildChannelChoices(nc); 

	// Set sliders, min/max values, etc. 
	for(int n=0; n<fvimp.getNumberChannels();++n) {
	    FluoviewTiff_.CProps cp = fvimp.getChannelProps(n); 

	    if(n==nCurrentChannel) {
		minRoV = cp.getMin(); 
		maxRoV = cp.getMax(); 
	    }
	}

	plotHistogram(fvimp); 
	updateSliders(); 
    }

    // --------------------------------------------------
    private void buildChannelChoices(int n) {
	String strBase = "Channel "; 
	for(int i=0; i<n;++i) {
	    String strChoice = strBase+i; 
	    chChannels.add(strChoice); 
	}
    }

    // --------------------------------------------------
    private void updateMin(FluoviewTiff_ fvimp) {
	fvimp.setChannelPropMin(nCurrentChannel,minRoV); 
    }

    // --------------------------------------------------
    private void updateMax(FluoviewTiff_ fvimp) {
	fvimp.setChannelPropMax(nCurrentChannel,maxRoV); 
    }

    // --------------------------------------------------
    private void updateChannel(FluoviewTiff_ fvimp) {
	FluoviewTiff_.CProps cp = 
	    fvimp.getChannelProps(nCurrentChannel); 
	minRoV = cp.getMin();
	maxRoV = cp.getMax();

	// Reset histogram.
	rovPlot.setHistogram(fvimp.getStatistics(nCurrentChannel)); 

	// Change sliders.
	updateSliders(); 
    }

    // --------------------------------------------------
    private void plotUpdate(FluoviewTiff_ fvimp) {
	// Update image parameters.
	updateMin(fvimp); 
	updateMax(fvimp); 

	updateSliders(); 
    }


    static final int REGION = 30; 
    private boolean bMinMove = false, bMaxMove = false; 

    // --------------------------------------------------
    public void mousePressed(MouseEvent e) {
	// Get x-coordinate of mouse. Determine if this is close
	// enough to either min or max rov line to constitute a
	// selection. Note, this has a bias. If close together, we
	// select minRoV control.
	int xmin = minRoV*SIZE/INTENSITY_MAX; 
	int xmax = maxRoV*SIZE/INTENSITY_MAX; 

	if(Math.abs(e.getX()-xmin) <= REGION) {
	    bMinMove = true;   bMaxMove = false; 
	}
	else if(Math.abs(e.getX()-xmax) <= REGION) {
	    bMaxMove = true;    bMinMove = false; 
	}
    }

    // --------------------------------------------------
    public void mouseReleased(MouseEvent e) {
	if(bMinMove==true || bMaxMove==true) {
	    plotChange((int)minRoV,(int)maxRoV); 
	}

	bMinMove = false; 
	bMaxMove = false; 
    }

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


    // --------------------------------------------------
    public void mouseDragged(MouseEvent e) {

	int rov=0; 
	if(bMaxMove==true) {
	    // min/max tests used to ensure value stays w/in bounds.
	    // maxRoV control for both adjuster and plot object
	    // updated here.
	    maxRoV = Math.max(0,e.getX()*(INTENSITY_MAX/SIZE)); 
	    rovPlot.maxRoV = maxRoV = Math.min(maxRoV,INTENSITY_MAX); 
	}
	else if(bMinMove==true) {
	    // min/max tests used to ensure value stays w/in bounds.
	    // minRoV control for both adjuster and plot object
	    // updated here.
	    minRoV = Math.max(0,e.getX()*(INTENSITY_MAX/SIZE));  
	    rovPlot.minRoV = minRoV = Math.min(minRoV,INTENSITY_MAX); 
	}

	if(bMaxMove==true || bMinMove==true) {
	    // Set clipping rectangle on plot graphics.
	    rovPlot.repaint(); 
	}
    }


    public void mouseMoved(MouseEvent e) { }

    // --------------------------------------------------
    public void processWindowEvent(WindowEvent e) {
	super.processWindowEvent(e); 
	if(e.getID()==WindowEvent.WINDOW_CLOSING)
	    finished(); 
    }

}   // RoVAdjuster


// --------------------------------------------------
/** RoVPlot plots a graphical indication of an image Range of Values
    (RoV). It is based on the ImageJ ContrastPlot class. */
class RoVPlot extends Canvas
{ 
    //    static final int SIZE = 120;   // must be less than 127
    static final int SIZE=256; 
    public double minRoV = 0;
    public double maxRoV = 4095;

    boolean bMinMove = false; 
    boolean bMaxMove = false; 

    private RoVAdjuster rovAdjuster; 

    int[] histogram;
    int hmax;
    
    // Support offscreen rendering to improve drawing performance.
    private Image offImage; 
    private Graphics offGraphics; 
    private Dimension offDimension; 

    // --------------------------------------------------
    public RoVPlot() {
	setSize(SIZE+1, SIZE+1);
    }


    // --------------------------------------------------
    void setHistogram(ImageStatistics stats) {
	int maxCount2 = 0;
	histogram = stats.histogram;
	for (int i = 0; i < stats.nBins; i++)
	    if ((histogram[i] > maxCount2) && (i != stats.mode))
		maxCount2 = histogram[i];
	hmax = stats.maxCount;
	if ((hmax>(maxCount2 * 2)) && (maxCount2 != 0)) {
	    hmax = (int)(maxCount2 * 1.5);
	    histogram[stats.mode] = hmax;
	}

	if(offImage==null) {
	    // Lazy construction idiom.
	    offDimension = getSize(); 
	    offImage = createImage(offDimension.width,offDimension.height); 
	    offGraphics = offImage.getGraphics();
	}

	repaint(); 
    }

    // --------------------------------------------------
    public void paint(Graphics g) {
	// Make sure we draw entire scene.
	g.setClip(0,0,SIZE+1,SIZE+1); 

	if(offImage != null) {
	    // Blit offscreen buffer to display buffer.
	    g.drawImage(offImage,0,0,null); 
	}
    }



    // --------------------------------------------------
    private void updateLines(Graphics g,
			     double xmin,double xmax) {
	Color color = g.getColor(); 

	// Draw new lines.
	g.setColor(Color.blue); g.drawLine((int)xmin,0,(int)xmin,SIZE); 
	g.setColor(Color.green); g.drawLine((int)xmax,0,(int)xmax,SIZE); 

	g.setColor(color); 
    }


    // --------------------------------------------------
    public void update(Graphics g) {
	// Erase previous off screen image.
	offGraphics.setColor(getBackground()); 
	offGraphics.fillRect(0,0,offDimension.width,offDimension.height); 
	offGraphics.setColor(Color.black); 

	// Paint scene to offscreen buffer.
	paintScene(offGraphics); 
	paint(g); 
    }


    // --------------------------------------------------
    public void paintScene(Graphics g) {
	g.drawRect(0,0,SIZE,SIZE); 

	// Draw lines indicating current max/min values.
	double xmin = (minRoV/RoVAdjuster.INTENSITY_MAX)*SIZE; 
	double xmax = (maxRoV/RoVAdjuster.INTENSITY_MAX)*SIZE; 

	updateLines(g,xmin,xmax); 

        // Draw histogram.
	if (histogram!=null && g!=null) {
	    for (int i = 0; i < SIZE; i++){
		g.drawLine(i, SIZE, i, 
			   SIZE - ((int)(SIZE * histogram[i/**2*/])/hmax));
	    }
	}
    }

}  // RoVPlot

