package ij.plugin;
import java.awt.*;
import java.io.*;
import java.util.*;
import ij.*;
import ij.gui.*;
import ij.io.*;
import ij.util.*;
import ij.plugin.frame.Editor;
import ij.text.TextWindow;
import java.awt.event.KeyEvent;

/** Compiles and runs plugins using the javac compiler. */
public class Compiler implements PlugIn, FilenameFilter {

	private static final int TARGET14=0, TARGET15=1, TARGET16=2,  TARGET17=3;
	private static final String[] targets = {"1.4", "1.5", "1.6", "1.7"};
	private static final String TARGET_KEY = "javac.target";
	private static com.sun.tools.javac.Main javac;
	private static ByteArrayOutputStream output;
	private static String dir, name;
	private static Editor errors;
	private static boolean generateDebuggingInfo;
	private static int target = (int)Prefs.get(TARGET_KEY, TARGET15);

	public void run(String arg) {
		if (arg.equals("edit"))
			edit();
		else if (arg.equals("options"))
			showDialog();
		else
			compileAndRun(arg);
	 }
	 
	void edit() {
		if (open("", "Open macro or plugin")) {
			Editor ed = (Editor)IJ.runPlugIn("ij.plugin.frame.Editor", "");
			if (ed!=null) ed.open(dir, name);
		}
	}
	
	void compileAndRun(String path) {
		if (!open(path, "Compile and Run Plugin..."))
			return;
		if (name.endsWith(".class")) {
			runPlugin(name.substring(0, name.length()-1));
			return;
		}
		if (!isJavac()) return;
		if (compile(dir+name))
			runPlugin(name);
	}
	 
	boolean isJavac() {
		try {
			if (javac==null) {
				output = new ByteArrayOutputStream(4096);
				javac=new com.sun.tools.javac.Main();
			}
		} catch (NoClassDefFoundError e) {
			IJ.error("Unable to find the javac compiler, which comes with the Windows and \n"
					+"Linux versions of ImageJ that include Java in the ImageJ/jre folder.\n"
 					+" \n"
 					+"   java.home: "+System.getProperty("java.home")
 					);
 			return false;
		}
		return true;
	}

	boolean compile(String path) {
		IJ.showStatus("compiling: "+path);
		output.reset();
		String classpath = getClassPath(path);
		Vector v = new Vector();
		if (generateDebuggingInfo)
			v.addElement("-g");
		if (IJ.isJava15()) {
			validateTarget();
			v.addElement("-source");
			v.addElement(targets[target]);
			v.addElement("-target");
			v.addElement(targets[target]);
			v.addElement("-Xlint:unchecked");
		}
		v.addElement("-deprecation");
		v.addElement("-classpath");
		v.addElement(classpath);
		v.addElement(path);
		String[] arguments = new String[v.size()];
		v.copyInto((String[])arguments);
		if (IJ.debugMode) {
			String str = "javac";
			for (int i=0; i<arguments.length; i++)
				str += " "+arguments[i];
			IJ.log(str);
		}
		boolean compiled = javac.compile(arguments, new PrintWriter(output))==0;
		String s = output.toString();
		boolean errors = (!compiled || areErrors(s));
		if (errors)
			showErrors(s);
		else
			IJ.showStatus("done");
		return compiled;
	 }
	 
	 // Returns a string containing the Java classpath, 
	 // the path to the directory containing the plugin, 
	 // and paths to any .jar files in the plugins folder.
	 String getClassPath(String path) {
		long start = System.currentTimeMillis();
	 	StringBuffer sb = new StringBuffer();
		sb.append(System.getProperty("java.class.path"));
		File f = new File(path);
		if (f!=null)  // add directory containing file to classpath
			sb.append(File.pathSeparator + f.getParent());
		String pluginsDir = Menus.getPlugInsPath();
		if (pluginsDir!=null)
			addJars(pluginsDir, sb);
		return sb.toString();
	 }
	 
	// Adds .jar files in plugins folder, and subfolders, to the classpath
	void addJars(String path, StringBuffer sb) {
		String[] list = null;
		File f = new File(path);
		if (f.exists() && f.isDirectory())
			list = f.list();
		if (list==null) return;
		if (!path.endsWith(File.separator))
			path += File.separator;
		for (int i=0; i<list.length; i++) {
			File f2 = new File(path+list[i]);
			if (f2.isDirectory())
				addJars(path+list[i], sb);
			else if (list[i].endsWith(".jar")&&(list[i].indexOf("_")==-1||list[i].equals("loci_tools.jar"))) {
				sb.append(File.pathSeparator+path+list[i]);
				if (IJ.debugMode) IJ.log("javac: "+path+list[i]);
			}
		}
	}

	boolean areErrors(String s) {
		boolean errors = s!=null && s.length()>0;
		if(errors && s.indexOf("1 warning")>0 && s.indexOf("[deprecation] show()")>0)
			errors = false;
		//if(errors&&s.startsWith("Note:com.sun.tools.javac")&&s.indexOf("error")==-1)
		//	errors = false;
		return errors;
	}
	
	void showErrors(String s) {
		if (errors==null || !errors.isVisible()) {
			errors = (Editor)IJ.runPlugIn("ij.plugin.frame.Editor", "");
			errors.setFont(new Font("Monospaced", Font.PLAIN, 12));
		}
		if (errors!=null)
			errors.display("Errors", s);
		IJ.showStatus("done (errors)");
	}

	 // open the .java source file
	 boolean open(String path, String msg) {
	 	boolean okay;
		String fileName, directory;
	 	if (path.equals("")) {
			if (dir==null) dir = IJ.getDirectory("plugins");
			OpenDialog od = new OpenDialog(msg, dir, name);
			directory = od.getDirectory();
			fileName = od.getFileName();
			okay = fileName!=null;
			String lcName = okay?fileName.toLowerCase(Locale.US):null;
			if (okay) {
				if (msg.startsWith("Compile")) {
					if (!(lcName.endsWith(".java")||lcName.endsWith(".class"))) {
						IJ.error("File name must end with \".java\" or \".class\".");
						okay = false;
					}
				} else if (!(lcName.endsWith(".java")||lcName.endsWith(".txt")||lcName.endsWith(".ijm")||lcName.endsWith(".js"))) {
					IJ.error("File name must end with \".java\", \".txt\" or \".js\".");
					okay = false;
				}
			}
		} else {
			int i = path.lastIndexOf('/');
			if (i==-1)
				i = path.lastIndexOf('\\');
			if (i>0) {
				directory = path.substring(0, i+1);
				fileName = path.substring(i+1);
			} else {
				directory = "";
				fileName = path;
			}
			okay = true;
		}
		if (okay) {
			name = fileName;
			dir = directory;
			Editor.setDefaultDirectory(dir);
		}
		return okay;
	}

	// only show files with names ending in ".java"
	// doesn't work with Windows
	public boolean accept(File dir, String name) {
		return name.endsWith(".java")||name.endsWith(".macro")||name.endsWith(".txt");
	}
	
	// run the plugin using a new class loader
	void runPlugin(String name) {
		name = name.substring(0,name.length()-5); // remove ".java"
		new PlugInExecuter(name);
	}
	
	public void showDialog() {
		validateTarget();
		GenericDialog gd = new GenericDialog("Compile and Run");
		gd.addChoice("Target: ", targets, targets[target]);
		gd.setInsets(15,5,0);
		gd.addCheckbox("Generate debugging info (javac -g)", generateDebuggingInfo);
        gd.addHelp(IJ.URL+"/docs/menus/edit.html#compiler");
		gd.showDialog();
		if (gd.wasCanceled()) return;
		target = gd.getNextChoiceIndex();		
		generateDebuggingInfo = gd.getNextBoolean();
		validateTarget();
	}
	
	void validateTarget() {
		if (target<0 || target>TARGET17)
			target = TARGET15;
		if ((target>TARGET16&&!IJ.isJava17()) || (target>TARGET15&&!IJ.isJava16()))
			target = TARGET15;
		if (!IJ.isJava15())
			target = TARGET14;
		Prefs.set(TARGET_KEY, target);
	}
	
}


class PlugInExecuter implements Runnable {

	private String plugin;
	private Thread thread;

	/** Create a new object that runs the specified plugin
		in a separate thread. */
	PlugInExecuter(String plugin) {
		this.plugin = plugin;
		thread = new Thread(this, plugin);
		thread.setPriority(Math.max(thread.getPriority()-2, Thread.MIN_PRIORITY));
		thread.start();
	}

	public void run() {
		try {
			IJ.resetEscape();
			IJ.runPlugIn("ij.plugin.ClassChecker", "");
			ImageJ ij = IJ.getInstance();
			if (ij!=null) ij.runUserPlugIn(plugin, plugin, "", true);
		} catch(Throwable e) {
			IJ.showStatus("");
			IJ.showProgress(1.0);
			ImagePlus imp = WindowManager.getCurrentImage();
			if (imp!=null) imp.unlock();
			String msg = e.getMessage();
			if (e instanceof RuntimeException && msg!=null && e.getMessage().equals(Macro.MACRO_CANCELED))
				return;
			IJ.handleException(e);
		}
	}

}
