package ij.plugin;
import ij.*;
import ij.gui.*;
import ij.process.*;
import ij.measure.*;
import ij.plugin.frame.Recorder;
import ij.plugin.filter.PlugInFilter;
import ij.plugin.frame.ThresholdAdjuster;
import java.awt.*;
import java.awt.event.*;
import java.util.*;

/** This plugin implements the Process/Binary/Make Binary 
	and Convert to Mask commands. */
public class Thresholder implements PlugIn, Measurements, ItemListener {
	
	public static final String[] methods = AutoThresholder.getMethods();
	public static final String[] backgrounds = {"Default", "Dark", "Light"};
	private double minThreshold;
	private double maxThreshold;
	private boolean autoThreshold;
	private boolean showLegacyDialog = true;
	private static boolean fill1 = true;
	private static boolean fill2 = true;
	private static boolean useBW = true;
	private boolean useLocal = true;
	private boolean listThresholds;
	private boolean createStack;
	private boolean convertToMask;
	private String method = methods[0];
	private String background = backgrounds[0];
	private static boolean staticUseLocal = true;
	private static boolean staticListThresholds;
	private static boolean staticCreateStack;
	private static String staticMethod = methods[0];
	private static String staticBackground = backgrounds[0];
	private ImagePlus imp;
	private Vector choices;

	public void run(String arg) {
		convertToMask = arg.equals("mask");
		if (arg.equals("skip") || convertToMask)
			showLegacyDialog = false;
		ImagePlus imp = IJ.getImage();
		if (imp.getStackSize()==1) {
			Undo.setup(Undo.TRANSFORM, imp);
			applyThreshold(imp, false);
		} else
			convertStack(imp);
		IJ.showProgress(1.0);
	}
	
	void convertStack(ImagePlus imp) {
		showLegacyDialog = false;
		boolean thresholdSet = imp.isThreshold();
		this.imp = imp;
		if (!IJ.isMacro()) {
			method = staticMethod;
			background = staticBackground;
			useLocal = staticUseLocal;
			listThresholds = staticListThresholds;
			createStack = staticCreateStack;
			if (!thresholdSet)
				updateThreshold(imp);
		} else {
			String macroOptions = Macro.getOptions();
			if (macroOptions!=null && macroOptions.indexOf("slice ")!=-1)
				Macro.setOptions(macroOptions.replaceAll("slice ", "only "));
			useLocal = false;
		}
		boolean saveBlackBackground = Prefs.blackBackground;
		boolean oneSlice = false;
		GenericDialog gd = new GenericDialog("Convert Stack to Binary");
		gd.addChoice("Method:", methods, method);
		gd.addChoice("Background:", backgrounds, background);
		gd.addCheckbox("Calculate threshold for each image", useLocal);
		gd.addCheckbox("Only convert current image", oneSlice);
		gd.addCheckbox("Black background (of binary masks)", Prefs.blackBackground);
		gd.addCheckbox("List thresholds", listThresholds);
		gd.addCheckbox("Create new stack", createStack);
		choices = gd.getChoices();
		if (choices!=null) {
			((Choice)choices.elementAt(0)).addItemListener(this);
			((Choice)choices.elementAt(1)).addItemListener(this);
		}
		gd.showDialog();
		if (gd.wasCanceled())
			return;
		this.imp = null;
		gd.setSmartRecording(method.equals("Default")?true:false);
		int index = gd.getNextChoiceIndex();
		method = methods[index];
		gd.setSmartRecording(false);
		gd.setSmartRecording(background.equals("Default")?true:false);
		index = gd.getNextChoiceIndex();
		background = backgrounds[index];
		gd.setSmartRecording(false);
		useLocal = gd.getNextBoolean();
		oneSlice = gd.getNextBoolean();
		Prefs.blackBackground = gd.getNextBoolean();
		listThresholds = gd.getNextBoolean();
		createStack = gd.getNextBoolean();
		if (imp.getStack().isVirtual()) {
			oneSlice = false;
			createStack = true;
		}
		if (!IJ.isMacro()) {
			staticMethod = method;
			staticBackground = background;
			staticUseLocal = useLocal;
			staticListThresholds = listThresholds;
			staticCreateStack = createStack;
		}
		if (oneSlice) {
			useLocal = false;
			listThresholds = false;
			createStack = false;
			if (oneSlice && imp.getBitDepth()!=8) {
				IJ.error("Thresholder", "8-bit stack required to process a single slice.");
				return;
			}
		}
		Undo.reset();
		ImageProcessor ip = imp.getProcessor();
		double minThreshold = ip.getMinThreshold();
		double maxThreshold = ip.getMaxThreshold();
		int currentSlice = imp.getCurrentSlice();
		if (createStack) {
			imp = imp.duplicate();
			imp.setTitle(imp.getTitle().replace("DUP_","MASK_"));
			if (minThreshold!=ImageProcessor.NO_THRESHOLD)
				imp.getProcessor().setThreshold(minThreshold,maxThreshold,ImageProcessor.RED_LUT);
			imp.setSlice(currentSlice);
		}
		if (useLocal)
			convertStackToBinary(imp);
		else
			applyThreshold(imp, oneSlice);
		Prefs.blackBackground = saveBlackBackground;
		if (createStack) {
			imp.setSlice(1);  //why is this needed
			imp.show();
			imp.setSlice(currentSlice);
		} else if (thresholdSet) {
			if (imp.getProcessor().getLutUpdateMode()!=ImageProcessor.NO_LUT_UPDATE)
				imp.getProcessor().resetThreshold();
			imp.updateAndDraw();
		}
	}

	private void applyThreshold(ImagePlus imp, boolean oneSlice) {
		imp.deleteRoi();
		ImageProcessor ip = imp.getProcessor();
		ip.resetBinaryThreshold();  // remove any invisible threshold set by Make Binary or Convert to Mask
		int type = imp.getType();
		if (type==ImagePlus.GRAY16 || type==ImagePlus.GRAY32) {
			applyShortOrFloatThreshold(imp);
			return;
		}
		if (!imp.lock()) return;
		double saveMinThreshold = ip.getMinThreshold();
		double saveMaxThreshold = ip.getMaxThreshold();
		autoThreshold = saveMinThreshold==ImageProcessor.NO_THRESHOLD;
					
		boolean useBlackAndWhite = false;
		boolean fill1 = true;
		boolean fill2 = true;
		String options = Macro.getOptions();
		boolean modernMacro = options!=null && !(options.contains("thresholded")||options.contains("remaining"));
		if (autoThreshold || modernMacro || (IJ.macroRunning()&&options==null))
			showLegacyDialog = false;
		int fcolor=255, bcolor=0;
			
		if (showLegacyDialog) {
			GenericDialog gd = new GenericDialog("Make Binary");
			gd.addCheckbox("Thresholded pixels to foreground color", fill1);
			gd.addCheckbox("Remaining pixels to background color", fill2);
			gd.addMessage("");
			gd.addCheckbox("Black foreground, white background", useBW);
			gd.showDialog();
			if (gd.wasCanceled())
				{imp.unlock(); return;}
			fill1 = gd.getNextBoolean();
			fill2 = gd.getNextBoolean();
			useBW = useBlackAndWhite = gd.getNextBoolean();
			int savePixel = ip.getPixel(0,0);
			if (useBlackAndWhite)
				ip.setColor(Color.black);
			else
				ip.setColor(Toolbar.getForegroundColor());
			ip.drawPixel(0,0);
			fcolor = ip.getPixel(0,0);
			if (useBlackAndWhite)
				ip.setColor(Color.white);
			else
				ip.setColor(Toolbar.getBackgroundColor());
			ip.drawPixel(0,0);
			bcolor = ip.getPixel(0,0);
			ip.setColor(Toolbar.getForegroundColor());
			ip.putPixel(0,0,savePixel);
		} else
			convertToMask = true;

		if (type==ImagePlus.COLOR_RGB) {
			ImageProcessor ip2 = updateColorThresholdedImage(imp);
			if (ip2!=null) {
				imp.setProcessor(ip2);
				autoThreshold =false;
				saveMinThreshold = 255;
				saveMaxThreshold = 255;
				type = ImagePlus.GRAY8;
			}
		}
		if (type!=ImagePlus.GRAY8)
			convertToByte(imp);
		ip = imp.getProcessor();
		
		if (autoThreshold)
			autoThreshold(ip);
		else {
			if (Recorder.record && !Recorder.scriptMode() && (!IJ.isMacro()||Recorder.recordInMacros))
				Recorder.record("//setThreshold", (int)saveMinThreshold, (int)saveMaxThreshold);
 			minThreshold = saveMinThreshold;
 			maxThreshold = saveMaxThreshold;
		}

		if (convertToMask && ip.isColorLut())
			ip.setColorModel(ip.getDefaultColorModel());
		ip.resetThreshold();

		if (IJ.debugMode) IJ.log("Thresholder (apply): "+minThreshold+"-"+maxThreshold+" "+fcolor+" "+bcolor+" "+fill1+" "+fill2);
		int[] lut = new int[256];
		for (int i=0; i<256; i++) {
			if (i>=minThreshold && i<=maxThreshold)
				lut[i] = fill1?fcolor:(byte)i;
			else {
				lut[i] = fill2?bcolor:(byte)i;
			}
		}
		if (imp.getStackSize()>1 && !oneSlice)
			new StackProcessor(imp.getStack(), ip).applyTable(lut);
		else
			ip.applyTable(lut);
		if (convertToMask && !oneSlice) {
			boolean invertedLut = imp.isInvertedLut();
			if ((invertedLut && Prefs.blackBackground) || (!invertedLut && !Prefs.blackBackground)) {
				ip.invertLut();
				if (!IJ.isMacro() && ThresholdAdjuster.isDarkBackground() && !invertedLut && !Prefs.blackBackground)
					IJ.log("\"Black background\" not set in Process>Binary>Options; inverting LUT");
				if (IJ.debugMode) IJ.log("Thresholder (inverting lut)");
			}
		}
		if (fill1 && fill2 && ((fcolor==0&&bcolor==255)||(fcolor==255&&bcolor==0))) {
			imp.getProcessor().setThreshold(fcolor, fcolor, ImageProcessor.NO_LUT_UPDATE);
			if (IJ.debugMode) IJ.log("Thresholder: "+fcolor+"-"+fcolor+" ("+(Prefs.blackBackground?"black":"white")+" background)");
		}
		imp.updateAndRepaintWindow();
		imp.unlock();
	}
	
	private ImageProcessor updateColorThresholdedImage(ImagePlus imp) {
		Object mask = imp.getProperty("Mask");
		if (mask==null || !(mask instanceof ByteProcessor))
			return null;
		ImageProcessor maskIP = (ImageProcessor)mask;
		if (maskIP.getWidth()!=imp.getWidth() || maskIP.getHeight()!=imp.getHeight())
			return null;
		Object originalImage = imp.getProperty("OriginalImage");
		if (originalImage!=null && (originalImage instanceof ImagePlus)) {
			ImagePlus imp2 = (ImagePlus)originalImage;
			if (imp2.getBitDepth()==24 && imp2.getWidth()==imp.getWidth() && imp2.getHeight()==imp.getHeight()) {
				imp.setProcessor(imp2.getProcessor());
				Undo.setup(Undo.TRANSFORM, imp);
			}
		}
		return maskIP;
	}
	
	private void applyShortOrFloatThreshold(ImagePlus imp) {
		if (!imp.lock()) return;
		int width = imp.getWidth();
		int height = imp.getHeight();
		int size = width*height;
		boolean isFloat = imp.getType()==ImagePlus.GRAY32;
		int currentSlice =  imp.getCurrentSlice();
		int nSlices = imp.getStackSize();
		ImageStack stack1 = imp.getStack();
		ImageStack stack2 = new ImageStack(width, height);
		ImageProcessor ip = imp.getProcessor();
		float t1 = (float)ip.getMinThreshold();
		float t2 = (float)ip.getMaxThreshold();
		if (t1==ImageProcessor.NO_THRESHOLD) {
			double min = ip.getMin();
			double max = ip.getMax();
			ip = ip.convertToByte(true);
			autoThreshold(ip);
			t1 = (float)(min + (max-min)*(minThreshold/255.0));
			t2 = (float)(min + (max-min)*(maxThreshold/255.0));
		}
		float value;
		ImageProcessor ip1, ip2;
		IJ.showStatus("Converting to mask");
		for (int i=1; i<=nSlices; i++) {
			IJ.showProgress(i, nSlices);
			String label = stack1.getSliceLabel(i);
			ip1 = stack1.getProcessor(i);
			ip2 = new ByteProcessor(width, height);
			for (int j=0; j<size; j++) {
				value = ip1.getf(j);
				if (value>=t1 && value<=t2)
					ip2.set(j, 255);
				else
					ip2.set(j, 0);
			}
			stack2.addSlice(label, ip2);
		}
		imp.setStack(null, stack2);
		ImageStack stack = imp.getStack();
		stack.setColorModel(LookUpTable.createGrayscaleColorModel(!Prefs.blackBackground));
		imp.setStack(null, stack);
		if (imp.isComposite()) {
			CompositeImage ci = (CompositeImage)imp;
			ci.setMode(IJ.GRAYSCALE);
			ci.resetDisplayRanges();
			ci.updateAndDraw();
		}
		imp.getProcessor().setThreshold(255, 255, ImageProcessor.NO_LUT_UPDATE);
		if (IJ.debugMode) IJ.log("Thresholder16: 255-255 ("+(Prefs.blackBackground?"black":"white")+" background)");
		IJ.showStatus("");
		imp.unlock();
	}

	void convertStackToBinary(ImagePlus imp) {
		int nSlices = imp.getStackSize();
		double[] minValues = listThresholds?new double[nSlices]:null;
		double[] maxValues = listThresholds?new double[nSlices]:null;
		int bitDepth = imp.getBitDepth();
		if (bitDepth!=8) {
			IJ.showStatus("Converting to byte");
			ImageStack stack1 = imp.getStack();
			ImageStack stack2 = new ImageStack(imp.getWidth(), imp.getHeight());
			for (int i=1; i<=nSlices; i++) {
				IJ.showProgress(i, nSlices);
				String label = stack1.getSliceLabel(i);
				ImageProcessor ip = stack1.getProcessor(i);
				ip.resetMinAndMax();
				if (listThresholds) {
					minValues[i-1] = ip.getMin();
					maxValues[i-1] = ip.getMax();
				}
				stack2.addSlice(label, ip.convertToByte(true));
			}
			imp.setStack(null, stack2);
		}
		ImageStack stack = imp.getStack();
		IJ.showStatus("Auto-thresholding");
		if (listThresholds)
			IJ.log("Thresholding method: "+method);
		for (int i=1; i<=nSlices; i++) {
			IJ.showProgress(i, nSlices);
			ImageProcessor ip = stack.getProcessor(i);
			if (method.equals("Default") && background.equals("Default"))
				ip.setAutoThreshold(ImageProcessor.ISODATA2, ImageProcessor.NO_LUT_UPDATE);
			else
				ip.setAutoThreshold(method, !background.equals("Light"), ImageProcessor.NO_LUT_UPDATE);
			minThreshold = ip.getMinThreshold();
			maxThreshold = ip.getMaxThreshold();
			if (listThresholds) {
				double t1 = minThreshold;
				double t2 = maxThreshold;
				if (bitDepth!=8) {
					t1 = minValues[i-1] + (t1/255.0)*(maxValues[i-1]-minValues[i-1]);
					t2 = minValues[i-1] + (t2/255.0)*(maxValues[i-1]-minValues[i-1]);
				}
				int digits = bitDepth==32?2:0;
				IJ.log("  "+i+": "+IJ.d2s(t1,digits)+"-"+IJ.d2s(t2,digits));
			}
			int[] lut = new int[256];
			for (int j=0; j<256; j++) {
				if (j>=minThreshold && j<=maxThreshold)
					lut[j] = (byte)255;
				else
					lut[j] = 0;
			}
			ip.applyTable(lut);
		}
		stack.setColorModel(LookUpTable.createGrayscaleColorModel(!Prefs.blackBackground));
		imp.setStack(null, stack);
		imp.getProcessor().setThreshold(255, 255, ImageProcessor.NO_LUT_UPDATE);
		if (imp.isComposite()) {
			CompositeImage ci = (CompositeImage)imp;
			ci.setMode(IJ.GRAYSCALE);
			ci.resetDisplayRanges();
			ci.updateAndDraw();
		}
		IJ.showStatus("");
	}

	void convertToByte(ImagePlus imp) {
		ImageProcessor ip;
		int currentSlice =  imp.getCurrentSlice();
		ImageStack stack1 = imp.getStack();
		ImageStack stack2 = imp.createEmptyStack();
		int nSlices = imp.getStackSize();
		String label;
		for(int i=1; i<=nSlices; i++) {
			label = stack1.getSliceLabel(i);
			ip = stack1.getProcessor(i);
			ip.setMinAndMax(0, 255);
			stack2.addSlice(label, ip.convertToByte(true));
		}
		imp.setStack(null, stack2);
		imp.setSlice(currentSlice);
		imp.setCalibration(imp.getCalibration()); //update calibration
	}
	
	/** Returns an 8-bit binary (0 and 255) threshold mask
	 * that has the same dimensions as this image.
	 * @see ij.process.ImageProcessor#createMask
	 * @see ij.ImagePlus#createThresholdMask
	 * @see ij.ImagePlus#createRoiMask
	*/
	public static ByteProcessor createMask(ImagePlus imp) {
		ImageProcessor ip = imp.getProcessor();
		if (ip instanceof ColorProcessor)
			throw new IllegalArgumentException("Non-RGB image requires");
		if (ip.getMinThreshold()==ImageProcessor.NO_THRESHOLD)
			throw new IllegalArgumentException("Image must be thresholded");
		return ip.createMask();
	}
	
	void autoThreshold(ImageProcessor ip) {
		ip.setAutoThreshold(ImageProcessor.ISODATA2, ImageProcessor.NO_LUT_UPDATE);
		minThreshold = ip.getMinThreshold();
		maxThreshold = ip.getMaxThreshold();
		if (IJ.debugMode) IJ.log("Thresholder (auto): "+minThreshold+"-"+maxThreshold);
 	}
 	
 	public static void setMethod(String method) {
 		staticMethod = method;
 	}

 	public static void setBackground(String background) {
 		staticBackground = background;
 	}

	public void itemStateChanged(ItemEvent e) {
		if (imp==null)
			return;
		Choice choice = (Choice)e.getSource();
		if (choice==choices.elementAt(0))
			method = choice.getSelectedItem();
		else
			background = choice.getSelectedItem();
		updateThreshold(imp);
	}
	
	private void updateThreshold(ImagePlus imp) {
		ImageProcessor ip = imp.getProcessor();
		if (method.equals("Default") && background.equals("Default"))
			ip.setAutoThreshold(ImageProcessor.ISODATA2, ImageProcessor.RED_LUT);
		else
			ip.setAutoThreshold(method, !background.equals("Light"), ImageProcessor.RED_LUT);
		imp.updateAndDraw();
	}

}
