package ij.plugin;
import ij.*;
import ij.process.*;
import ij.gui.*;
import ij.util.Tools;
import ij.util.StringSorter;
import ij.io.*;
import ij.macro.Interpreter;
import java.awt.*;
import java.awt.event.*;
import java.io.*;
import java.util.Vector;

/** This plugin implements the File/Batch/Macro and File/Batch/Virtual Stack commands. */
	public class BatchProcessor implements PlugIn, ActionListener, ItemListener, Runnable {
		private static final String MACRO_FILE_NAME = "BatchMacro.ijm";
		private static final String[] formats = {"TIFF", "8-bit TIFF", "JPEG", "GIF", "PNG", "PGM", "BMP", "FITS", "Text Image", "ZIP", "Raw"};
		private static String format = Prefs.get("batch.format", formats[0]);
		
		private static final String[] code = {
			"[Select from list]",
			"Add Border",
			"Convert to RGB",
			"Crop",
			"Gaussian Blur",
			"Invert",
			"Label",
			"Timestamp",
			"Max Dimension",
			"Measure",
			"Print Index and Title",
			"Resize",
			"Scale",
			"Show File Info",
			"Unsharp Mask",
		};
		
		private static final String help = "<html>"
		+"<h1>Process&gt;Batch&gt;Virtual Stack</h1>"
		+"<font size=+1>"
		+"This command runs macro code on each image in a virtual stack.<br>"
		+"The processed images are saved in the <i>Output</i> folder,<br>"
		+"in the specified <i>Format</i>, allowing them to be opened as a<br>"
		+"virtual stack. Make sure the <i>Output</i> folder is empty<br>"
		+"before clicking on <i>Process</i>.<br>"
		+"<br>"
		+"In the macro code, the 'i' (slice index) and 'n' (stack size) variables<br>"
		+"are predefined. Call <i>setOption('SaveBatchOutput',false)</i> to<br>"
		+"prevent the image currently being processed from being saved,<br>"
		+"effectively removing it from the output virtual stack.<br> <br>"
		+"</font>";

		private String macro = "";
		private int testImage;
		private Button input, output, open, save, test;
		private TextField inputDir, outputDir;
		private GenericDialog gd;
		private Thread thread;
		private ImagePlus virtualStack;
		private ImagePlus outputImage;
		private boolean errorDisplayed;
		private String filter;
		private static boolean saveOutput = true;

	public void run(String arg) {
		if (arg.equals("stack")) {
			virtualStack = IJ.getImage();
			if (virtualStack.getStackSize()==1) {
				error("This command requires a stack or virtual stack.");
				return;
			}
		}
		String macroPath = IJ.getDirectory("macros")+MACRO_FILE_NAME;
		macro = IJ.openAsString(macroPath);
		if (macro==null || macro.startsWith("Error: ")) {
			IJ.showStatus(macro.substring(7) + ": "+macroPath);
			macro = "";
		}
		if (!showDialog()) return;
		String inputPath = null;
		if (virtualStack==null) {
			inputPath = inputDir.getText();
			if (inputPath.equals("")) {
				error("Please choose an input folder");
				return;
			}
			inputPath = addSeparator(inputPath);
			File f1 = new File(inputPath);
			if (!f1.exists() || !f1.isDirectory()) {
				error("Input does not exist or is not a folder\n \n"+inputPath);
				return;
			}
		}
		String outputPath = outputDir.getText();
		outputPath = addSeparator(outputPath);
		File f2 = new File(outputPath);
		if (!outputPath.equals("") && (!f2.exists() || !f2.isDirectory())) {
			error("Output does not exist or is not a folder\n \n"+outputPath);
			return;
		}
		if (macro.equals("")) {
			error("There is no macro code in the text area");
			return;
		}
		ImageJ ij = IJ.getInstance();
		if (ij!=null) ij.getProgressBar().setBatchMode(true);
		IJ.resetEscape();
		if (virtualStack!=null)
			processVirtualStack(outputPath);
		else
			processFolder(inputPath, outputPath);
		IJ.showProgress(1,1);
		if (virtualStack==null)
			Prefs.set("batch.input", inputDir.getText());
		Prefs.set("batch.output", outputDir.getText());
		Prefs.set("batch.format", format);
		macro = gd.getTextArea1().getText();
		if (!macro.equals(""))
			IJ.saveString(macro, IJ.getDirectory("macros")+MACRO_FILE_NAME);
	}
		
	boolean showDialog() {
		validateFormat();
		gd = GUI.newNonBlockingDialog("Batch Process");
		addPanels(gd);
		gd.setInsets(15, 0, 5);
		gd.addChoice("Output_format:", formats, format);
		gd.setInsets(0, 0, 5);
		gd.addChoice("Add macro code:", code, code[0]);
		if (virtualStack==null)
			gd.addStringField("File name contains:", "", 10);
		gd.setInsets(15, 10, 0);
		Dimension screen = IJ.getScreenSize();
		gd.addTextAreas(macro, null, screen.width<=600?10:15, 60);
		addButtons(gd);
		gd.setOKLabel("Process");
		Vector choices = gd.getChoices();
		Choice choice = (Choice)choices.elementAt(1);
		if (virtualStack!=null)
			gd.addHelp(help);
		choice.addItemListener(this);
		gd.showDialog();
		format = gd.getNextChoice();
		if (virtualStack==null)
			filter = gd.getNextString();
		macro = gd.getNextText();
		return !gd.wasCanceled();
	}
	
	void processVirtualStack(String outputPath) {
		ImageStack stack = virtualStack.getStack();
		int n = stack.size();
		int index = 0;
		for (int i=1; i<=n; i++) {
			if (IJ.escapePressed()) break;
			IJ.showProgress(i, n);
			ImageProcessor ip = stack.getProcessor(i);
			if (ip==null) return;
			ImagePlus imp = new ImagePlus(i+"/"+stack.size(), ip);
			if (!macro.equals("")) {
				if (!runMacro("i="+(index++)+";"+"n="+stack.size()+";"+macro, imp))
					break;
			}
			if (saveOutput && !outputPath.equals("")) {
				if (format.equals("8-bit TIFF") || format.equals("GIF")) {
					if (imp.getBitDepth()==24)
						IJ.run(imp, "8-bit Color", "number=256");
					else
						IJ.run(imp, "8-bit", "");
				}
				IJ.saveAs(imp, format, outputPath+pad(i));
			}
			saveOutput = true;
			imp.close();
		}
		if (outputPath!=null && !outputPath.equals(""))
			IJ.run("Image Sequence...", "open=[" + outputPath + "]"+" use");
	}
	
	String pad(int n) {
		String str = ""+n;
		while (str.length()<5)
		str = "0" + str;
		return str;
	}

	
	void processFolder(String inputPath, String outputPath) {
		String[] list = (new File(inputPath)).list();
		list = FolderOpener.getFilteredList(list, filter, "Batch Processor");
		if (list==null)
			return;
		StringSorter.sort(list);
		int index = 0;
		int startingCount = WindowManager.getImageCount();
		for (int i=0; i<list.length; i++) {
			if (IJ.escapePressed()) break;
			String path = inputPath + list[i];
			if (IJ.debugMode) IJ.log(i+": "+path);
			if ((new File(path)).isDirectory())
				continue;
			if (list[i].startsWith(".")||list[i].endsWith(".avi")||list[i].endsWith(".AVI") || list[i].equals("Thumbs.db"))
				continue;
			IJ.showProgress(i+1, list.length);
			IJ.redirectErrorMessages(true);
			ImagePlus imp = IJ.openImage(path);
			IJ.redirectErrorMessages(false);
			if (imp==null && WindowManager.getImageCount()>startingCount)
				imp = WindowManager.getCurrentImage();
			if (imp==null)
				imp = Opener.openUsingBioFormats(path);
			if (imp==null) {
				IJ.log("openImage() and openUsingBioFormats() returned null: "+path);
				continue;
			}
			if (!macro.equals("")) {
				outputImage = null;
				if (!runMacro("i="+(index++)+";"+macro, imp))
					break;
			}
			if (saveOutput && !outputPath.equals("")) {
				if (format.equals("8-bit TIFF") || format.equals("GIF")) {
					if (imp.getBitDepth()==24)
						IJ.run(imp, "8-bit Color", "number=256");
					else
						IJ.run(imp, "8-bit", "");
				}
				if (outputImage!=null && outputImage!=imp)
					IJ.saveAs(outputImage, format, outputPath+list[i]);
				else
					IJ.saveAs(imp, format, outputPath+list[i]);
			}
			saveOutput = true;
			imp.close();
		}
	}
	
	private boolean runMacro(String macro, ImagePlus imp) {
		WindowManager.setTempCurrentImage(imp);
		Interpreter interp = new Interpreter();
		try {
			outputImage = interp.runBatchMacro(macro, imp);
		} catch(Throwable e) {
			interp.abortMacro();
			String msg = e.getMessage();
			if (!(e instanceof RuntimeException && msg!=null && e.getMessage().equals(Macro.MACRO_CANCELED)))
				IJ.handleException(e);
			return false;
		} finally {
			WindowManager.setTempCurrentImage(null);
		}
		return true;
	}
		
	String addSeparator(String path) {
		if (path.equals("")) return path;
		if (!(path.endsWith("/")||path.endsWith("\\")))
			path = path + File.separator;
		return path;
	}
	
	void validateFormat() {
		boolean validFormat = false;
		for (int i=0; i<formats.length; i++) {
			if (format.equals(formats[i])) {
				validFormat = true;
				break;
			}
		}
		if (!validFormat) format = formats[0];
	}

	void addPanels(GenericDialog gd) {
		Panel p = new Panel();
    	p.setLayout(new FlowLayout(FlowLayout.CENTER, 5, 0));
		if (virtualStack==null) {
			input = new Button("Input...");
			input.addActionListener(this);
			p.add(input);
			inputDir = new TextField(Prefs.get("batch.input", ""), 45);
			p.add(inputDir);
			gd.addPanel(p);
		}
		p = new Panel();
    	p.setLayout(new FlowLayout(FlowLayout.CENTER, 5, 0));
		output = new Button("Output...");
		output.addActionListener(this);
		p.add(output);
		outputDir = new TextField(Prefs.get("batch.output", ""), 45);
		p.add(outputDir);
		gd.addPanel(p);
	}
	
	void addButtons(GenericDialog gd) {
		Panel p = new Panel();
    	p.setLayout(new FlowLayout(FlowLayout.CENTER, 5, 0));
		test = new Button("Test");
		test.addActionListener(this);
		p.add(test);
		open = new Button("Open...");
		open.addActionListener(this);
		p.add(open);
		save = new Button("Save...");
		save.addActionListener(this);
		p.add(save);
		gd.addPanel(p);
	}

	public void itemStateChanged(ItemEvent e) {
		Choice choice = (Choice)e.getSource();
		String item = choice.getSelectedItem();
		String code = null;
		if (item.equals("Convert to RGB"))
			code = "run(\"RGB Color\");\n";
		else if (item.equals("Measure"))
			code = "run(\"Measure\");\n";
		else if (item.equals("Resize"))
			code = "run(\"Size...\", \"width=512 height=512 interpolation=Bicubic\");\n";
		else if (item.equals("Scale"))
			code = "scale=1.5;\nw=getWidth*scale; h=getHeight*scale;\nrun(\"Size...\", \"width=w height=h interpolation=Bilinear\");\n";
		else if (item.equals("Label"))
			code = "setFont(\"SansSerif\", 18, \"antialiased\");\nsetColor(\"red\");\ndrawString(\"Hello\", 20, 30);\n";
		else if (item.equals("Timestamp"))
			code = openMacroFromJar("TimeStamp.ijm");
		else if (item.equals("Crop"))
			code = "makeRectangle(getWidth/4, getHeight/4, getWidth/2, getHeight/2);\nrun(\"Crop\");\n";
		else if (item.equals("Add Border"))
			code = "border=25;\nw=getWidth+border*2; h=getHeight+border*2;\nrun(\"Canvas Size...\", \"width=w height=h position=Center zero\");\n";
		else if (item.equals("Invert"))
			code = "run(\"Invert\");\n";
		else if (item.equals("Gaussian Blur"))
			code = "run(\"Gaussian Blur...\", \"sigma=2\");\n";
		else if (item.equals("Unsharp Mask"))
			code = "run(\"Unsharp Mask...\", \"radius=1 mask=0.60\");\n";
		else if (item.equals("Show File Info"))
			code = "path=File.directory+File.name;\ndate=File.dateLastModified(path);\nsize=File.length(path);\nprint(i+\", \"+getTitle+\", \"+date+\", \"+size);\n";
		else if (item.equals("Max Dimension"))
			code = "max=2048;\nw=getWidth; h=getHeight;\nsize=maxOf(w,h);\nif (size>max) {\n  scale = max/size;\n  w*=scale; h*=scale;\n  run(\"Size...\", \"width=w height=h interpolation=Bicubic average\");\n}";
		else if (item.equals("Print Index and Title"))
			code =  "if (i==0) print(\"\\\\Clear\"); print(IJ.pad(i,4)+\": \"+getTitle());\n";
		if (code!=null) {
			TextArea ta = gd.getTextArea1();
			ta.insert(code, ta.getCaretPosition());
			if (IJ.isMacOSX()) ta.requestFocus();
		}
	}

	public static String openMacroFromJar(String name) {
		ImageJ ij = IJ.getInstance();
		Class c = ij!=null?ij.getClass():(new ImageStack()).getClass();
		String macro = null;
        try {
			InputStream is = c .getResourceAsStream("/macros/"+name);
			if (is==null) return null;
            InputStreamReader isr = new InputStreamReader(is);
            StringBuffer sb = new StringBuffer();
            char [] b = new char [8192];
            int n;
            while ((n = isr.read(b)) > 0)
                sb.append(b,0, n);
            macro = sb.toString();
        }
        catch (IOException e) {
        	return null;
        }
        return macro;
	}

	public void actionPerformed(ActionEvent e) {
		Object source = e.getSource();
		if (source==input) {
			String path = IJ.getDirectory("Input Folder");
			if (path==null) return;
			inputDir.setText(path);
		} else if (source==output) {
			String path = IJ.getDirectory("Output Folder");
			if (path==null) return;
			outputDir.setText(path);
		} else if (source==test) {
			thread = new Thread(this, "BatchTest"); 
			thread.setPriority(Math.max(thread.getPriority()-2, Thread.MIN_PRIORITY));
			thread.start();
		} else if (source==open)
			open();
		else if (source==save)
			save();
	}
	
	void open() {
		String text = IJ.openAsString("");
		if (text==null) return;
		if (text.startsWith("Error: "))
			error(text.substring(7));
		else {
			if (text.length()>30000)
				error("File is too large");
			else
				gd.getTextArea1().setText(text);
		}
	}
	
	void save() {
		macro = gd.getTextArea1().getText();
		if (!macro.equals(""))
			IJ.saveString(macro, "");
	}

	void error(String msg) {
		IJ.error("Batch Processor", msg);
	}
	
	public void run() {
		TextArea ta = gd.getTextArea1();
		//ta.selectAll();
		String macro = ta.getText();
		if (macro.equals("")) {
			error("There is no macro code in the text area");
			return;
		}
		ImagePlus imp = null;
		IJ.redirectErrorMessages(true);
		if (virtualStack!=null)
			imp = getVirtualStackImage();
		else
			imp = getFolderImage();
		IJ.redirectErrorMessages(false);
		if (imp==null) {
			if (!errorDisplayed)
				IJ.log("IJ.openImage() returned null");
			return;
		}
		runMacro("i=0;"+macro, imp);
		Point loc = new Point(10, 30);
		if (testImage!=0) {
			ImagePlus imp2 = WindowManager.getImage(testImage);
			if (imp2!=null) {
				ImageWindow win = imp2.getWindow();
				if (win!=null) loc = win.getLocation();
				imp2.changes=false;
				imp2.close();
			}
		}
		imp.show();
		ImageWindow iw = imp.getWindow();
		if (iw!=null) iw.setLocation(loc);
		testImage = imp.getID();
	}
	
	ImagePlus getVirtualStackImage() {
		ImagePlus imp = virtualStack.createImagePlus();
		imp.setProcessor("", virtualStack.getProcessor().duplicate());
		return imp;
	}

	ImagePlus getFolderImage() {
		String inputPath = inputDir.getText();
		inputPath = addSeparator(inputPath);
		File f1 = new File(inputPath);
		if (!f1.exists() || !f1.isDirectory()) {
			error("Input does not exist or is not a folder\n \n"+inputPath);
			errorDisplayed = true;
			return null;
		}
		String[] list = (new File(inputPath)).list();
		String name = list[0];
		if (name.startsWith(".")&&list.length>1) name = list[1];
		String path = inputPath + name;
		setDirAndName(path);
		return IJ.openImage(path);
	}
	
	void setDirAndName(String path) {
		File f = new File(path);
		OpenDialog.setLastDirectory(f.getParent()+File.separator);
		OpenDialog.setLastName(f.getName());
	}
	
	public static void saveOutput(boolean b) {
		saveOutput = b;
	}

}
