package ij.plugin;
import ij.*;
import ij.gui.*;
import ij.io.*;
import ij.plugin.frame.Editor;
import ij.plugin.Macro_Runner;
import ij.plugin.filter.PlugInFilter;
import ij.plugin.filter.PlugInFilterRunner;
import java.awt.Font;
import java.lang.reflect.Method;
import java.io.*;
import java.util.*;
import javax.tools.*;

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

	private static final String info = 
		"Library JAR files (e.g., imagescience.jar) should\n"
		+"be located in either plugins/jars or plugins/lib.\n \n"
		+"The javac command line will be displayed in\n"
		+"the Log window if ImageJ is in debug mode.";
	private static final String TARGET_KEY = "javac.target";
	private static CompilerTool compilerTool;
	private static String dir, name;
	private static Editor errors;
	private static boolean generateDebuggingInfo;
	private static boolean checkForUpdateDone;
	private static int target = (int)Prefs.get(TARGET_KEY, 8);	

	public void run(String arg) {
		if (arg.equals("edit"))
			edit();
		else if (arg.equals("options"))
			showDialog();
		else {
			if (arg!=null && arg.length()>0 && !arg.endsWith(".java"))
				IJ.error("Compiler", "File name must end with \".java\"");
			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()) {
			if (IJ.debugMode) IJ.log("Compiler: javac not found");
			if (!checkForUpdateDone) {
				checkForUpdate("/plugins/compiler/Compiler.jar", "1.48c");
				checkForUpdateDone = true;
			}
			Object compiler = IJ.runPlugIn("Compiler", dir+name);
			if (compiler==null) {
				boolean ok = Macro_Runner.downloadJar("/plugins/compiler/Compiler.jar");
				if (ok)
					IJ.runPlugIn("Compiler", dir+name);
			}
			return;
		}
		if (compile(dir+name))
			runPlugin(name);
	}
	
	private void checkForUpdate(String plugin, String currentVersion) {
		int slashIndex = plugin.lastIndexOf("/");
		if (slashIndex==-1 || !plugin.endsWith(".jar"))
			return;
		String className = plugin.substring(slashIndex+1, plugin.length()-4);
		File f = new File(Prefs.getImageJDir()+"plugins"+File.separator+"jars"+File.separator+className+".jar");
		if (!f.exists() || !f.canWrite()) {
			if (IJ.debugMode) IJ.log("checkForUpdate: jar not found ("+plugin+")");
			return;
		}
		String version = null;
		try {
			Class c = IJ.getClassLoader().loadClass("Compiler");
			version = "0.00a";
			Method m = c.getDeclaredMethod("getVersion", new Class[0]);
			version = (String)m.invoke(null, new Object[0]);
		}
		catch (Exception e) {}
		if (version==null) {
			if (IJ.debugMode) IJ.log("checkForUpdate: class not found ("+className+")");
			return;
		}
		if (version.compareTo(currentVersion)>=0) {
			if (IJ.debugMode) IJ.log("checkForUpdate: up to date ("+className+"  "+version+")");
			return;
		}
		boolean ok = Macro_Runner.downloadJar(plugin);
		if (IJ.debugMode) IJ.log("checkForUpdate: "+className+" "+version+" "+ok);
	}
	 
	boolean isJavac() {
		if (compilerTool==null)
			compilerTool=CompilerTool.getDefault();
		return compilerTool!=null;
	}

	boolean compile(String path) {
		IJ.showStatus("compiling "+path);
		String classpath = getClassPath(path);
		Vector options = new Vector();
		if (generateDebuggingInfo)
			options.addElement("-g");
		validateTarget();
		options.addElement("-source");
		options.addElement(getTargetAsString());
		options.addElement("-target");
		options.addElement(getTargetAsString());
		options.addElement("-Xlint:unchecked");
		options.addElement("-deprecation");
		options.addElement("-classpath");
		options.addElement(classpath);
		
		Vector sources = new Vector();
		sources.add(path);
		
		if (IJ.debugMode) {
			StringBuilder builder = new StringBuilder();
			builder.append("javac");
			for (int i=0; i< options.size(); i++){
				builder.append(" ");
				builder.append(options.get(i));
			}
			for (int i=0; i< sources.size(); i++){
				builder.append(" ");
				builder.append(sources.get(i));
			}
			IJ.log(builder.toString());
		}
		
		boolean errors = true;
		String s = "not compiled";
		if (compilerTool != null) {
			final StringWriter outputWriter = new StringWriter();
			errors = !compilerTool.compile(sources, options, outputWriter);
			s = outputWriter.toString();
		} else {
			errors = true;
		}
		
		if (errors)
			showErrors(s);
		else
			IJ.showStatus("done");
		return !errors;
	 }
	 
	 // 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;
		boolean isJarsFolder = path.endsWith("jars")|| path.endsWith("lib");
		path = IJ.addSeparator(path);
		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].contains("_")||isJarsFolder)) {
				sb.append(File.pathSeparator+path+list[i]);
				//IJ.log("javac classpath: "+path+list[i]);
			}
		}
	}
	
	void showErrors(String s) {
		if (errors==null || !errors.isVisible()) {
			errors = (Editor)IJ.runPlugIn("ij.plugin.frame.Editor", "");
			errors.setFont(new Font("Monospaced", Font.PLAIN, errors.getFontSize()));
		}
		if (errors!=null) {
			ImageJ ij = IJ.getInstance();
			if (ij!=null)
				s = ij.getInfo()+"\n \n"+s;
			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")||lcName.endsWith(".py")||lcName.endsWith(".bsh"))) {
					IJ.error("File name must end with \".java\", \".txt\", \".ijm\", \".js\" or \".py\"");
					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" or ".clas"
		new PlugInExecuter(name);
	}
	
	public void showDialog() {
		validateTarget();
		GenericDialog gd = new GenericDialog("Compile and Run");
		gd.addNumericField("Target: ", target, 0, 3, "");
		gd.setInsets(15,5,0);
		gd.addCheckbox("Generate debugging info (javac -g)", generateDebuggingInfo);
		gd.addHelp(IJ.URL2+"/docs/menus/edit.html#compiler");
		Font font = IJ.font10;
		gd.addMessage(info, font);
		gd.showDialog();
		if (gd.wasCanceled()) return;
		target = (int)gd.getNextNumber();		
		generateDebuggingInfo = gd.getNextBoolean();
		validateTarget();
	}
	
	void validateTarget() {
		if (target==4 || target==5) //Java 8 or 9
			target = 8;
		if (target<6)
			target = 6;
		if (target>IJ.javaVersion())
			target = IJ.javaVersion();
		Prefs.set(TARGET_KEY, target);
	}
	
	private static String getTargetAsString() {
		if (target<8)
			return "1.6";
		else if (target==8)
			return "1.8";
		else
			return ""+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() {
		IJ.resetEscape();
		IJ.runPlugIn("ij.plugin.ClassChecker", "");
		runCompiledPlugin(plugin);
	}
	
	void runCompiledPlugin(String className) {
		if (IJ.debugMode) IJ.log("Compiler: running \""+className+"\"");
		IJ.resetClassLoader();
		ClassLoader loader = IJ.getClassLoader();
		Object thePlugIn = null;
		try { 
			thePlugIn = (loader.loadClass(className)).newInstance(); 
			if (thePlugIn instanceof PlugIn)
 				((PlugIn)thePlugIn).run("");
 			else if (thePlugIn instanceof PlugInFilter)
				new PlugInFilterRunner(thePlugIn, className, "");
		}
		catch (ClassNotFoundException e) {
			if (className.indexOf('_')!=-1)
				IJ.error("Plugin or class not found: \"" + className + "\"\n(" + e+")");
		}
		catch (NoClassDefFoundError e) {
			String err = e.getMessage();
			if (IJ.debugMode) IJ.log("NoClassDefFoundError: "+err);
			int index = err!=null?err.indexOf("wrong name: "):-1;
			if (index>-1 && !className.contains(".")) {
				String className2 = err.substring(index+12, err.length()-1);
				className2 = className2.replace("/", ".");
				if (className2.equals(className)) { // Java 9 error format different
					int spaceIndex = err.indexOf(" ");
					if (spaceIndex>-1) {
						className2 = err.substring(0, spaceIndex);
						className2 = className2.replace("/", ".");
					}
				}
				if (className2.equals(className))
					IJ.error("Plugin not found: "+className2);
				else
					runCompiledPlugin(className2);
				return;
			}
			if (className.indexOf('_')!=-1)
				IJ.error("Plugin or class not found: \"" + className + "\"\n(" + e+")");
		}
		catch (Exception e) {
			//IJ.error(""+e);
			IJ.handleException(e); //Marcel Boeglin 2013.09.01
			//Logger.getLogger(getClass().getName()).log(Level.SEVERE, null, e); //IDE output
		}
	}
	
}

abstract class CompilerTool {

	public static class JavaxCompilerTool extends CompilerTool {

		public boolean compile(List sources, List options, StringWriter log) {
			if (IJ.debugMode) IJ.log("Compiler: using javax.tool.JavaCompiler");
			try {
				JavaCompiler javac = getJavac();
				DiagnosticCollector<JavaFileObject> diagnostics = new DiagnosticCollector<JavaFileObject>();
				StandardJavaFileManager fileManager = javac.getStandardFileManager(diagnostics, null, null);
				Iterable<? extends JavaFileObject> compilationUnits = fileManager.getJavaFileObjectsFromStrings(sources);
				JavaCompiler.CompilationTask task =javac.getTask(log, fileManager, null, options, null, compilationUnits);
    			fileManager.close();
    			return task.call();
			} catch (Exception e) {
				PrintWriter printer = new PrintWriter(log);
				e.printStackTrace(printer);
				printer.flush();
			}
			return false;
		}

		protected JavaCompiler getJavac() throws Exception {
			JavaCompiler javac = ToolProvider.getSystemJavaCompiler();
			return javac;
		}
	}

	public static class LegacyCompilerTool extends CompilerTool {
		protected static Class javacC;

		public boolean compile(List sources, List options, StringWriter log) {
			if (IJ.debugMode) IJ.log("Compiler: using com.sun.tools.javac");
			try {
				final String[] args = new String[sources.size() + options.size()];
				int argsIndex = 0;
				for (int optionsIndex = 0; optionsIndex < options.size(); optionsIndex++)
					args[argsIndex++] = (String) options.get(optionsIndex);
				for (int sourcesIndex = 0; sourcesIndex < sources.size(); sourcesIndex++)
					args[argsIndex++] = (String) sources.get(sourcesIndex);
				PrintWriter printer = new PrintWriter(log);
				Object javac = getJavac();
				Class[] compileTypes = new Class[] { String[].class, PrintWriter.class };
				Method compile = javacC.getMethod("compile", compileTypes);
				Object result = compile.invoke(javac, new Object[] { args, printer });
				printer.flush();
				return Integer.valueOf(0).equals(result);
			} catch (Exception e) {
				e.printStackTrace(new PrintWriter(log));
			}
			return false;
		}

		protected Object getJavac() throws Exception {
			if (javacC==null)
				javacC = Class.forName("com.sun.tools.javac.Main");
			return javacC.newInstance();
		}
	}

	public static CompilerTool getDefault() {
		CompilerTool javax = new JavaxCompilerTool();
		if (javax.isSupported())
			return javax;
		CompilerTool legacy = new LegacyCompilerTool();
		if (legacy.isSupported())
			return legacy;
		return null;
	}

	public abstract boolean compile(List sources, List options, StringWriter log);

	protected abstract Object getJavac() throws Exception;

	public boolean isSupported() {
		try {
			return null != getJavac();
		} catch (Exception e) {
			return false;
		}
	}
}

