package ij.plugin.filter;
import ij.*;
import ij.process.*;
import ij.gui.*;
import ij.plugin.filter.PlugInFilter.*;
import ij.plugin.filter.*;
import ij.measure.Calibration;
import ij.macro.Interpreter;
import java.awt.*;
import java.util.*;

public class PlugInFilterRunner implements Runnable, DialogListener {
	private String command;					// the command, can be but need not be the name of the PlugInFilter
	private Object theFilter;					// the instance of the PlugInFilter
	private ImagePlus imp;
	private int flags;							// the flags returned by the PlugInFilter
	private Overlay originalOverlay;		// overlay before pressing 'preview', to revert
	private boolean previewCheckboxOn;			// the state of the preview checkbox (true = on)
	private boolean bgPreviewOn;		// tells the background thread that preview is allowed
	private boolean bgKeepPreview;		// tells the background thread to keep the result of preview
	private Thread previewThread;		 // the thread of the preview
	private GenericDialog gd;			 // the dialog (if it registers here by setDialog)
	private Checkbox previewCheckbox;	 // reference to the preview Checkbox of the dialog
	private long previewTime;				// time (ms) needed for preview processing
	private boolean ipChanged;			// whether the image data have been changed
	private int processedAsPreview;			// the slice processed during preview (if non-zero)
	private Object snapshotPixels;		// the snapshot to show we have one and for undo in case of parallel actions intervening
	private Hashtable<Thread, int[]> slicesForThread;		// gives first&last slice that a given thread should process
	private Hashtable<Thread, ImageProcessor> roisForThread;// gives ROI that a given thread should process
	Hashtable sliceForThread = new Hashtable(); // here the stack slice currently processed is stored.
	private int nPasses;						// the number of calls to the run(ip) method of the filter
	private int pass;						// passes done so far
	private boolean doStack;

	/** The constructor runs a PlugInFilter or ExtendedPlugInFilter by calling its
	 * setup, run, etc. methods. For details, see the documentation of interfaces
	 * PlugInFilter and ExtendedPlugInFilter.
	 * @param theFilter The PlugInFilter to be run
	 * @param command	The command that has caused running the PlugInFilter
	 * @param arg		The argument specified for this PlugInFilter in IJ_Props.txt or in the
	 * plugins.config file of a .jar archive conatining a collection of plugins. <code>arg</code>
	 * may be a string of length zero.
	 */
	public PlugInFilterRunner(Object theFilter, String command, String arg) {
		this.theFilter = theFilter;
		this.command = command;
		imp = WindowManager.getCurrentImage();
		flags = ((PlugInFilter)theFilter).setup(arg, imp);	// S E T U P
		if ((flags&PlugInFilter.DONE)!=0) return;
		if (!checkImagePlus(imp, flags, command)) return;	// check whether the PlugInFilter can handle this image type
		if ((flags&PlugInFilter.NO_IMAGE_REQUIRED)!=0)
			imp = null;									// if the plugin does not want an image, it should not get one
		Roi roi = null;
		if (imp != null) {
			roi = imp.getRoi();
			if (roi!=null) roi.endPaste();				// prepare the image: finish previous paste operation (if any)
			if (!imp.lock())
				return;					// exit if image is in use
			nPasses = ((flags&PlugInFilter.CONVERT_TO_FLOAT)!=0) ? imp.getProcessor().getNChannels():1;
		}
		if (theFilter instanceof ExtendedPlugInFilter) { // calling showDialog required?
			try {
				flags = ((ExtendedPlugInFilter)theFilter).showDialog(imp, command, this);  // D I A L O G (may include preview)
			} catch(Exception e) {
				killPreview();
				if (Macro.MACRO_CANCELED.equals(e.getMessage()))
					throw new RuntimeException(Macro.MACRO_CANCELED);
			}
			if (snapshotPixels != null)
				Undo.setup(Undo.FILTER, imp);			// ip has a snapshot that may be used for Undo
			boolean keepPreviewFlag = (flags&ExtendedPlugInFilter.KEEP_PREVIEW)!=0;
			if (keepPreviewFlag && imp!=null && previewThread!=null && ipChanged &&
					previewCheckbox!=null && previewCheckboxOn) {
				bgKeepPreview = true;
				waitForPreviewDone();
				processedAsPreview = imp.getCurrentSlice();
			} else {
				killPreview();
				previewTime = 0;
			}
		} // if ExtendedPlugInFilter
		if ((flags&PlugInFilter.DONE)!=0) {
			if (imp != null)
				imp.unlock();
			return;
		} else if (imp==null) {
			((PlugInFilter)theFilter).run(null);		// not DONE, but NO_IMAGE_REQUIRED
			return;
		}
		/* preparing for the run(ip) method of the PlugInFilter... */
		int slices = imp.getStackSize();
		//IJ.log("processedAsPreview="+processedAsPreview+"; slices="+slices+"; doesStacks="+((flags&PlugInFilter.DOES_STACKS)!=0));
		if ((flags&PlugInFilter.PARALLELIZE_IMAGES)!=0)
			flags &= ~PlugInFilter.PARALLELIZE_STACKS;
		doStack = slices>1 && (flags&PlugInFilter.DOES_STACKS)!=0;
		imp.startTiming();
		if (doStack || processedAsPreview==0) {				// if processing during preview was not enough
			//IJ.showStatus(command + (doStack ? " (Stack)..." : "..."));
			ImageProcessor ip = imp.getProcessor();
			pass = 0;
			if (!doStack) {	   // single image
				FloatProcessor fp = null;
				prepareProcessor(ip, imp);
				announceSliceNumber(imp.getCurrentSlice());
				if (theFilter instanceof ExtendedPlugInFilter)
					((ExtendedPlugInFilter)theFilter).setNPasses(nPasses);
				if ((flags&PlugInFilter.NO_CHANGES)==0) {	// for filters modifying the image
					boolean disableUndo = Prefs.disableUndo || (flags&PlugInFilter.NO_UNDO)!=0;
					if (!disableUndo || ((ip instanceof ColorProcessor)&&WindowManager.getWindow("B&C")!=null)) {
						ip.snapshot();
						snapshotPixels = ip.getSnapshotPixels();
					}
				}
				processOneImage(ip, fp, snapshotPixels);	// may also set class variable snapshotPixels
				if ((flags&PlugInFilter.NO_CHANGES)==0) {	// (filters doing no modifications don't change undo status)
					if (snapshotPixels != null) {
						ip.setSnapshotPixels(snapshotPixels);
						Undo.setup(Undo.FILTER, imp);
					} else
						Undo.reset();
				}
				if ((flags&PlugInFilter.NO_CHANGES)==0&&(flags&PlugInFilter.KEEP_THRESHOLD)==0)
					ip.resetBinaryThreshold();
			} else {  // stack
				if ((flags&PlugInFilter.NO_UNDO_RESET)==0)
					Undo.reset();	 // no undo for processing a complete stack
				IJ.resetEscape();
				int slicesToDo = processedAsPreview!=0 ? slices-1 : slices;
				nPasses *= slicesToDo;
				if (theFilter instanceof ExtendedPlugInFilter)
					((ExtendedPlugInFilter)theFilter).setNPasses(nPasses);
				int threads = 1;
				if ((flags&PlugInFilter.PARALLELIZE_STACKS)!=0) {
					threads = Prefs.getThreads(); // multithread support for multiprocessor machines
					if (threads>slicesToDo) threads = slicesToDo;
					if (threads>1) slicesForThread = new Hashtable<Thread, int[]>(threads-1);
				}
				int startSlice = 1;
				for (int i=1; i<threads; i++) {				// setup the background threads
					int endSlice = (slicesToDo*i)/threads;
					if (processedAsPreview!=0 && processedAsPreview<=endSlice) endSlice++;
					Thread bgThread = new Thread(this, command+" "+startSlice+"-"+endSlice);
					slicesForThread.put(bgThread, new int[] {startSlice, endSlice});
					bgThread.start();
					//IJ.log("Stack: Thread for slices "+startSlice+"-"+endSlice+" started");
					startSlice = endSlice+1;
				}
				//IJ.log("Stack: Slices "+startSlice+"-"+slices+" by main thread");
				processStack(startSlice, slices);			// the current thread does the rest
				if (slicesForThread != null) {
					while (slicesForThread.size()>0) {	  // for all other threads:
						Thread theThread = (Thread)slicesForThread.keys().nextElement();
						try {
							theThread.join();				// wait until thread has finished
						} catch (InterruptedException e) {}
						slicesForThread.remove(theThread);	// and remove it from the list.
					}
				}
			}
		} // end processing:
		if ((flags&PlugInFilter.FINAL_PROCESSING)!=0 && !IJ.escapePressed())
			((PlugInFilter)theFilter).setup("final", imp);
		if (IJ.escapePressed()) {
			IJ.showStatus(command + " INTERRUPTED");
			IJ.showProgress(1,1);
		} else
			IJ.showTime(imp, imp.getStartTime()-previewTime, command + ": ", doStack?slices:1);
		IJ.showProgress(1.0);
		if (ipChanged) {
			imp.changes = true;
			imp.updateAndDraw();
		}
		ImageWindow win = imp.getWindow();
		if (win!=null) {
			win.running = false;
			win.running2 = false;
		}
		imp.unlock();
	}

	/** Process a stack or part of it. The slice given by class variable
	 *	processedAsPreview remains unchanged.
	 * @param firstSlice Slice number of the first slice to be processed
	 * @param endSlice	 Slice number of the last slice to be processed
	 */
	private void processStack(int firstSlice, int endSlice) {
		ImageStack stack = imp.getStack();
		ImageProcessor ip = stack.getProcessor(firstSlice);
		prepareProcessor(ip, imp);
		ip.setLineWidth(Line.getWidth());		//in contrast to imp.getProcessor, stack.getProcessor does not do this
		FloatProcessor fp = null;
		int slices = imp.getNSlices();
		for (int i=firstSlice; i<=endSlice; i++) {
			if (i != processedAsPreview) {
				announceSliceNumber(i);
				ip.setPixels(stack.getPixels(i));
				ip.setSliceNumber(i);
				ip.setSnapshotPixels(null);
				processOneImage(ip, fp, null);
				if (IJ.escapePressed()) {IJ.beep(); break;}
			}
		}
	}

	/** prepare an ImageProcessor by setting roi and CalibrationTable.
	 */
	private void prepareProcessor(ImageProcessor ip, ImagePlus imp) {
		ImageProcessor mask = imp.getMask();
		Roi roi = imp.getRoi();
		if (roi!=null && roi.isArea())
			ip.setRoi(roi);
		else
			ip.setRoi((Roi)null);
		if (imp.getStackSize()>1) {
			ImageProcessor ip2 = imp.getProcessor();
			double min1 = ip2.getMinThreshold();
			double max1 = ip2.getMaxThreshold();
			double min2 = ip.getMinThreshold();
			double max2 = ip.getMaxThreshold();
			if (min1!=ImageProcessor.NO_THRESHOLD && (min1!=min2||max1!=max2))
				ip.setThreshold(min1, max1, ImageProcessor.NO_LUT_UPDATE);
		}
		//float[] cTable = imp.getCalibration().getCTable();
		//ip.setCalibrationTable(cTable);
	}

	/**
	 * Process a single image with the PlugInFilter.
	 * @param ip		The image data that should be processed
	 * @param fp		A Floatprocessor as a target for conversion to Float. May be null.
	 * @param snapshotPixels Valid snapshotPixels for the current ImageProcessor (null when processing a stack)
	 * Class variables used: flags (input), snapshotPixels (set if a snapshot of ip
	 * is taken), ipChanged (set if ip was probably changed).
	 */
	private void processOneImage(ImageProcessor ip, FloatProcessor fp, Object snapshotPixels) {
		if ((flags&PlugInFilter.PARALLELIZE_IMAGES)!=0) {
			processImageUsingThreads(ip, fp, snapshotPixels);
			return;
		}
		Thread thread = Thread.currentThread();
		boolean convertToFloat = (flags&PlugInFilter.CONVERT_TO_FLOAT)!=0 && !(ip instanceof FloatProcessor);
		boolean doMasking = (flags&PlugInFilter.SUPPORTS_MASKING)!=0 && ip.getMask() != null;
		if (snapshotPixels==null && (doMasking || ((flags&PlugInFilter.SNAPSHOT)!=0) && !convertToFloat)) {
			ip.snapshot();
			this.snapshotPixels = ip.getSnapshotPixels();
		}
		if (convertToFloat) {
			for (int i=0; i<ip.getNChannels(); i++) {
				fp = ip.toFloat(i, fp);
				fp.setSliceNumber(ip.getSliceNumber());
				if (thread.isInterrupted()) return;			// interrupt processing for preview?
				if ((flags&PlugInFilter.SNAPSHOT)!=0) fp.snapshot();
				if (doStack) IJ.showProgress(pass/(double)nPasses);
				((PlugInFilter)theFilter).run(fp);
				if (thread.isInterrupted()) return;
				//IJ.log("slice="+getSliceNumber()+" pass="+pass+"/"+nPasses);
				pass++;
				if ((flags&PlugInFilter.NO_CHANGES)==0) {
					ipChanged = true;
					ip.setPixels(i, fp);
				}
			}
		} else {
			if ((flags&PlugInFilter.NO_CHANGES)==0) ipChanged = true;
				if (doStack) IJ.showProgress(pass/(double)nPasses);
			((PlugInFilter)theFilter).run(ip);
			pass++;
		}

		if (thread.isInterrupted()) return;
		if (doMasking) {
			if (snapshotPixels != null)
				ip.setSnapshotPixels(snapshotPixels); //in case something has intervened and destroyed the snapshot
			ip.reset(ip.getMask());	 //restore image outside irregular roi
		}
   }

	private void processImageUsingThreads(ImageProcessor ip, FloatProcessor fp, Object snapshotPixels) {
		if (IJ.debugMode)
			IJ.log("using threads: "+ip.getNChannels());
		Thread thread = Thread.currentThread();
		boolean convertToFloat = (flags&PlugInFilter.CONVERT_TO_FLOAT)!=0 && !(ip instanceof FloatProcessor);
		boolean doMasking = (flags&PlugInFilter.SUPPORTS_MASKING)!=0 && ip.getMask() != null;
		if (snapshotPixels==null && (doMasking || ((flags&PlugInFilter.SNAPSHOT)!=0) && !convertToFloat)) {
			ip.snapshot();
			this.snapshotPixels = ip.getSnapshotPixels();
		}
		if (convertToFloat) {
			for (int i=0; i<ip.getNChannels(); i++) {
				fp = ip.toFloat(i, fp);
				fp.setSliceNumber(ip.getSliceNumber());
				if (thread.isInterrupted()) return;			// interrupt processing for preview?
				if ((flags&PlugInFilter.SNAPSHOT)!=0) fp.snapshot();
				if (doStack) IJ.showProgress(pass/(double)nPasses);
				processChannelUsingThreads(fp);
				if (thread.isInterrupted()) return;
				//IJ.log("slice="+getSliceNumber()+" pass="+pass+"/"+nPasses);
				if ((flags&PlugInFilter.NO_CHANGES)==0) {
					ipChanged = true;
					ip.setPixels(i, fp);
				}
			}
		} else {
			if ((flags&PlugInFilter.NO_CHANGES)==0) ipChanged = true;
				if (doStack) IJ.showProgress(pass/(double)nPasses);
			processChannelUsingThreads(ip);
		}
		if (thread.isInterrupted()) return;
		if (doMasking) {
			if (snapshotPixels != null)
				ip.setSnapshotPixels(snapshotPixels);
			ip.reset(ip.getMask());	 //restore image outside irregular roi
		}
	}

	/** process an image or a single color channel of an RGB image by splitting
	 *	into ROIs and using a separate thread for each roi */
	private void processChannelUsingThreads(ImageProcessor ip) {
		ImageProcessor mask = ip.getMask();
		Rectangle roi = ip.getRoi();
		int threads = Prefs.getThreads();
		if (IJ.debugMode)
			IJ.log("processing channel: "+threads);
		if (threads>roi.height) threads = roi.height;
		if (threads>1) roisForThread = new Hashtable<Thread, ImageProcessor>(threads-1);
		int y1 = roi.y;
		for (int i=1; i<threads; i++) {
			int y2 = roi.y+(roi.height*i)/threads-1;
			Thread bgThread = new Thread(this, command+" "+y1+"-"+y2);
			Rectangle roi2 = new Rectangle(roi.x, y1, roi.width, y2-y1+1);
			roisForThread.put(bgThread, duplicateProcessor(ip, roi2));
			bgThread.start();
			if (IJ.debugMode)
				IJ.log("  starting thread: "+y1+"-"+y2);
			y1 = y2+1;
		}
		if (IJ.debugMode)
			IJ.log("  main thread "+y1+"-"+(roi.y+roi.height));
		Rectangle roi2 = new Rectangle(roi.x, y1, roi.width, roi.y+roi.height-y1);
		((PlugInFilter)theFilter).run(duplicateProcessor(ip, roi2)); 	// current thread does the rest
		pass++;
		if (roisForThread != null) {
			for (Enumeration<Thread> en = roisForThread.keys(); en.hasMoreElements();) {
				Thread theThread = en.nextElement();
				try {
					theThread.join();		// wait until thread has finished
				} catch (InterruptedException e) { // if preview cancelled:
					interruptRoiThreads(roisForThread); //interrupt all threads and join
					Thread.currentThread().interrupt(); //restore 'interrupted' state
					break;
				}
			}
		}
		roisForThread = null;
		ip.setMask(mask);  // restore ROI
		ip.setRoi(roi);
	}

	ImageProcessor duplicateProcessor(ImageProcessor ip, Rectangle roi) {
		ImageProcessor ip2 = (ImageProcessor)ip.clone();
		ip2.setRoi(roi);
		return ip2;
	}

	/** interrupt threads processing the rois of an image and wait till they have finished */
	void interruptRoiThreads(Hashtable<Thread, ImageProcessor> roisForThread) {
		if (roisForThread==null) return;	//class variable may become null in other thread
		for (Enumeration<Thread> en = roisForThread.keys(); en.hasMoreElements();)
			((Thread)en.nextElement()).interrupt(); //interrupt all threads
		for (Enumeration<Thread> en = roisForThread.keys(); en.hasMoreElements();)
			try {
				((Thread)en.nextElement()).join();
			} catch (Exception e){}
	}

	/** test whether an ImagePlus can be processed based on the flags specified
	 *	and display an error message if not.
	 */
	private boolean checkImagePlus(ImagePlus imp, int flags, String cmd) {
		boolean imageRequired = (flags&PlugInFilter.NO_IMAGE_REQUIRED)==0;
		if (imageRequired && imp==null)
			{IJ.noImage(); return false;}
		if (imageRequired) {
			if (imp.getProcessor()==null)
				{wrongType(flags, cmd); return false;}
			int type = imp.getType();
			switch (type) {
				case ImagePlus.GRAY8:
					if ((flags&PlugInFilter.DOES_8G)==0)
					{wrongType(flags, cmd); return false;}
					break;
				case ImagePlus.COLOR_256:
					if ((flags&PlugInFilter.DOES_8C)==0)
					{wrongType(flags, cmd); return false;}
					break;
				case ImagePlus.GRAY16:
					if ((flags&PlugInFilter.DOES_16)==0)
					{wrongType(flags, cmd); return false;}
					break;
				case ImagePlus.GRAY32:
					if ((flags&PlugInFilter.DOES_32)==0)
					{wrongType(flags, cmd); return false;}
					break;
				case ImagePlus.COLOR_RGB:
					if ((flags&PlugInFilter.DOES_RGB)==0)
					{wrongType(flags, cmd); return false;}
					break;
			}
			if ((flags&PlugInFilter.ROI_REQUIRED)!=0 && imp.getRoi()==null)
			{IJ.error(cmd, "This command requires a selection"); return false;}
			if ((flags&PlugInFilter.STACK_REQUIRED)!=0 && imp.getStackSize()==1)
			{IJ.error(cmd, "This command requires a stack"); return false;}
		} // if imageRequired
		return true;
	}

	/** Display an error message, telling the allowed image types
	 */
	static void wrongType(int flags, String cmd) {
		String s = "\""+cmd+"\" requires an image of type:\n \n";
		if ((flags&PlugInFilter.DOES_8G)!=0) s +=  "	8-bit grayscale\n";
		if ((flags&PlugInFilter.DOES_8C)!=0) s +=  "	8-bit color\n";
		if ((flags&PlugInFilter.DOES_16)!=0) s +=  "	16-bit grayscale\n";
		if ((flags&PlugInFilter.DOES_32)!=0) s +=  "	32-bit (float) grayscale\n";
		if ((flags&PlugInFilter.DOES_RGB)!=0) s += "	RGB color\n";
		IJ.error(s);
	}

	/** Make the slice number accessible to the PlugInFilter by putting it
	 * into the appropriate hashtable.
	 */
	private void announceSliceNumber(int slice) {
		synchronized(sliceForThread){
			Integer number = new Integer(slice);
			sliceForThread.put(Thread.currentThread(), number);
		}
	}

	/** Return the slice number currently processed by the calling thread.
	 * @return The slice number. Returns -1 on error (when not processing).
	 */
	public int getSliceNumber() {
		synchronized(sliceForThread){
			Integer number = (Integer)sliceForThread.get(Thread.currentThread());
			return (number == null) ? -1 : number.intValue();
		}
	}

	/** The dispatcher for the background threads
	 */
	public void run() {
		Thread thread = Thread.currentThread();
		try {
			if (thread==previewThread)
				runPreview();
			else if (roisForThread!=null && roisForThread.containsKey(thread)) {
				ImageProcessor ip = (ImageProcessor)roisForThread.get(thread);
				((PlugInFilter)theFilter).run(ip);
				ip.setPixels(null);
				ip.setSnapshotPixels(null);
			} else if (slicesForThread!=null && slicesForThread.containsKey(thread)) {
				int[] range = (int[])slicesForThread.get(thread);
				processStack(range[0], range[1]);
			} else
				IJ.error("PlugInFilterRunner internal error:\nunsolicited background thread");
		} catch (Exception err) {
			if (thread==previewThread) {
				gd.previewRunning(false);
				IJ.wait(100); // needed on Macs
				previewCheckbox.setState(false);
				bgPreviewOn = false;
				previewThread = null;
			}
			String msg = ""+err;
			if (msg.indexOf(Macro.MACRO_CANCELED)==-1) {
				IJ.beep();
				IJ.log("ERROR: "+msg+"\nin "+thread.getName()+
					"\nat "+(err.getStackTrace()[0])+"\nfrom "+(err.getStackTrace()[1]));
			}
		}
	}

	/** The background thread for preview */
	private void runPreview() {
		if (IJ.debugMode)
			IJ.log("preview thread started; imp="+imp.getTitle());
		Thread thread = Thread.currentThread();
		ImageProcessor ip = imp.getProcessor();
		Roi originalRoi = imp.getRoi();
		originalOverlay = imp.getOverlay();
		FloatProcessor fp = null;
		prepareProcessor(ip, imp);
		announceSliceNumber(imp.getCurrentSlice());
		if (snapshotPixels==null && (flags&PlugInFilter.NO_CHANGES)==0) {
			ip.snapshot();
			snapshotPixels = ip.getSnapshotPixels();
		}
		boolean previewDataOk = false;
		while(bgPreviewOn) {
			if (previewCheckboxOn) gd.previewRunning(true); // visual feedback
			interruptable: {
				if (imp.getRoi() != originalRoi) {
					imp.setRoi(originalRoi);		// restore roi; the PlugInFilter may have affected it
					if (originalRoi!=null && originalRoi.isArea())
						ip.setRoi(originalRoi);
					else
						ip.setRoi((Roi)null);
				}
				if (ipChanged) {					// restore image data if necessary
					ip.setSnapshotPixels(snapshotPixels);
					ip.reset();
				}
				ipChanged = false;
				previewDataOk = false;
				long startTime = System.currentTimeMillis();
				pass = 0;
				if (theFilter instanceof ExtendedPlugInFilter)
					((ExtendedPlugInFilter)theFilter).setNPasses(nPasses); //this should reset pass in the filter
				if (thread.isInterrupted())
					break interruptable;
				//IJ.log("process preview start now");
				processOneImage(ip, fp, snapshotPixels); // P R O C E S S   (sets ipChanged)
				IJ.showProgress(1.0);
				if (thread.isInterrupted())
					break interruptable;
				previewDataOk = true;
				previewTime = System.currentTimeMillis() - startTime;
				imp.updateAndDraw();
				if (IJ.debugMode)
					IJ.log("preview processing done");
			}
			gd.previewRunning(false);				// optical feedback
			IJ.showStatus("");						//delete last status messages from processing
			synchronized(this) {
				if (!bgPreviewOn)
					break;							//thread should stop and possibly keep the data
				try {
					wait();							//wait for interrupted (don't keep preview) or notify (keep preview)
				} catch (InterruptedException e) {
					previewDataOk = false;
				}
			} // synchronized
		} // while bgPreviewOn
		if (thread.isInterrupted())
			previewDataOk = false;					//interrupted always means "don't keep preview"
		if (!previewDataOk || !bgKeepPreview) {		//no need to keep the result
			imp.setRoi(originalRoi);				//restore roi
			if (ipChanged) {						//revert the image data
				ip.setSnapshotPixels(snapshotPixels);
				ip.reset();
				ipChanged = false;
			}
		}
		imp.updateAndDraw();						//display current state of image (reset or result of preview)
		sliceForThread.remove(thread);				//no need to announce the slice number any more
	 }

	/** stop the background process responsible for preview as fast as possible
	 and wait until the preview thread has finished */
	private void killPreview() {
		if (previewThread == null) return;
		synchronized (this) {
			previewThread.interrupt();		//ask for premature finishing (interrupt first -> no keepPreview)
			bgPreviewOn = false;				//tell a possible background thread to terminate when it has finished
			if (roisForThread!=null)
				interruptRoiThreads(roisForThread);
		}
		waitForPreviewDone();
		imp.setOverlay(originalOverlay);
	}

	/** stop the background process responsible for preview and wait until the preview thread has finished */
	private void waitForPreviewDone() {
		if (previewThread.isAlive()) try {		//a NullPointerException is possible if the thread finishes in the meanwhile
			previewThread.setPriority(Thread.currentThread().getPriority());
		} catch (Exception e) {}
		synchronized (this) {
			bgPreviewOn = false;	 //tell a possible background thread to terminate
			notify();							//(but finish processing unless interrupted)
		}
		try {previewThread.join();}				//wait until the background thread is done
		catch (InterruptedException e){}
		previewThread = null;
	}

	/* set the GenericDialog gd where the preview comes from (if gd is
	 * suitable for listening, i.e., if it has a preview checkbox).
	 */
	public void setDialog(GenericDialog gd) {
		if (gd != null && imp != null) {
			previewCheckbox = gd.getPreviewCheckbox();
			if (previewCheckbox != null) {
				gd.addDialogListener(this);
				this.gd = gd;
			}
		} //IJ.log("setDialog done");
	}

	/** The listener to any change in the dialog. It is used for preview.
	 * It is invoked every time the user changes something in the dialog
	 * (except OK and cancel buttons), provided that all previous
	 * listeners (parameter checking) have returned true.
	 *
	 * @param e The event that has happened in the dialog. This method may
	 *			be also called with e=null, e.g. to start preview already
	 *			when the dialog appears.
	 * @return	Always true. (The return value determines whether the
	 *			dialog will enable the OK button)
	 */
	public boolean dialogItemChanged(GenericDialog gd, AWTEvent e) {
		if (previewCheckbox == null || imp == null) return true;
		previewCheckboxOn = previewCheckbox.getState();
		if (previewCheckboxOn  && previewThread == null) {
			bgPreviewOn = true;						//need to start a background thread for preview
			previewThread = new Thread(this, command+" Preview");
			int priority = Thread.currentThread().getPriority() - 2;
			if (priority < Thread.MIN_PRIORITY) priority = Thread.MIN_PRIORITY;
			previewThread.setPriority(priority);	//preview on lower priority than dialog
			previewThread.start();
			if (IJ.debugMode)
				IJ.log(command+" Preview thread was started");
			return true;
		}
		if (previewThread != null) {				//thread runs already
			if (!previewCheckboxOn) {				//preview toggled off
				killPreview();
				return true;
			} else
				previewThread.interrupt();	//all other changes: restart calculating preview (with new parameters)
		}
		return true;
	}

}
