/*******************************************************************************
 * Copyright (c) 2000, 2004 IBM Corporation and others.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v10.html
 *
 * Contributors:
 *     IBM Corporation - initial API and implementation
 *******************************************************************************/
package org.eclipse.jdt.core.tests.eval.target;

import java.lang.reflect.*;
import java.io.*;
import java.util.*;

/**
 * A code snippet runner loads code snippet classes and global
 * variable classes, and that run the code snippet classes.
 * <p>
 * When started, this runner first connects using TCP/IP to the provided port number.
 * If a regular classpath directory is provided, it writes the class definitions it gets from the IDE
 * to this directory (or to the bootclasspath directory if the class name starts with "java") and it
 * lets the system class loader (or the bootstrap class loader if it is a "java" class) load
 * the class.
 * If the regular classpath directory is null, it uses a code snippet class loader to load the classes
 * it gets from the IDE. 
 * <p>
 * IMPORTANT NOTE:
 * Using a code snippet class loader has the following limitation when the code snippet is ran:
 * <ul>
 * <li>The code snippet class can access only public classes, and public members or these classes.
 *	   This is because the "runtime package" of the code snippet class is always different from
 *	   the "runtime package" of the class it is trying to access since the class loaders are 
 * 	   different.
 * <li>The code snippet class cannot be defined in a "java.*" package. Only the bootstrap class 
 *	   loader can load such a class.
 * </ul> 
 */
public class CodeSnippetRunner {
	public static CodeSnippetRunner theRunner;
	static final String CODE_SNIPPET_CLASS_NAME = "org.eclipse.jdt.internal.eval.target.CodeSnippet";
	static final String RUN_METHOD_NAME = "run";
	static final String GET_RESULT_TYPE_METHOD_NAME = "getResultType";
	static final String GET_RESULT_VALUE_METHOD_NAME = "getResultValue";
	
	IDEInterface ide;
	String classPathDirectory;
	String bootclassPathDirectory;
	CodeSnippetClassLoader loader;
	Class codeSnippetClass = null;
/**
 * Creates a new code snippet runner. 
 */
public CodeSnippetRunner(int portNumber, String classPathDirectory, String bootclassPathDirectory) {
	this.ide = new IDEInterface(portNumber);
	if (classPathDirectory != null) {
		this.classPathDirectory = classPathDirectory;
		if (bootclassPathDirectory != null) {
			this.bootclassPathDirectory = bootclassPathDirectory;
		}
	} else {
		this.loader = new CodeSnippetClassLoader();
	}
}
/**
 * Returns the forward slash separated class name from the given class definition.
 */
private String className(byte[] classDefinition) {
	// NB: The following code was copied from org.eclipse.jdt.internal.compiler.cfmt, 
	//     thus it is highly dependent on the class file format.
	int readOffset = 10;
	try {
		int constantPoolCount = u2At(8, classDefinition);
		int[] constantPoolOffsets = new int[constantPoolCount];
		for (int i = 1; i < constantPoolCount; i++) {
			int tag = u1At(readOffset, classDefinition);
			switch (tag) {
				case 1 : // Utf8Tag
					constantPoolOffsets[i] = readOffset;
					readOffset += u2At(readOffset + 1, classDefinition);
					readOffset += 3; // ConstantUtf8.fixedSize
					break;
				case 3 : // IntegerTag
					constantPoolOffsets[i] = readOffset;
					readOffset += 5; // ConstantInteger.fixedSize
					break;
				case 4 : // FloatTag
					constantPoolOffsets[i] = readOffset;
					readOffset += 5; // ConstantFloat.fixedSize
					break;
				case 5 : // LongTag
					constantPoolOffsets[i] = readOffset;
					readOffset += 9; // ConstantLong.fixedSize
					i++;
					break;
				case 6 : // DoubleTag
					constantPoolOffsets[i] = readOffset;
					readOffset += 9; // ConstantDouble.fixedSize
					i++;
					break;
				case 7 : // ClassTag
					constantPoolOffsets[i] = readOffset;
					readOffset += 3; // ConstantClass.fixedSize
					break;
				case 8 : // StringTag
					constantPoolOffsets[i] = readOffset;
					readOffset += 3; // ConstantString.fixedSize
					break;
				case 9 : // FieldRefTag
					constantPoolOffsets[i] = readOffset;
					readOffset += 5; // ConstantFieldRef.fixedSize
					break;
				case 10 : // MethodRefTag
					constantPoolOffsets[i] = readOffset;
					readOffset += 5; // ConstantMethodRef.fixedSize
					break;
				case 11 : // InterfaceMethodRefTag
					constantPoolOffsets[i] = readOffset;
					readOffset += 5; // ConstantInterfaceMethodRef.fixedSize
					break;
				case 12 : // NameAndTypeTag
					constantPoolOffsets[i] = readOffset;
					readOffset += 5; // ConstantNameAndType.fixedSize
			}
		}
		// Skip access flags
		readOffset += 2;

		// Read the classname, use exception handlers to catch bad format
		int constantPoolIndex = u2At(readOffset, classDefinition);
		int utf8Offset = constantPoolOffsets[u2At(constantPoolOffsets[constantPoolIndex] + 1, classDefinition)];
		char[] className = utf8At(utf8Offset + 3, u2At(utf8Offset + 1, classDefinition), classDefinition);
		return new String(className);
	} catch (ArrayIndexOutOfBoundsException e) {
		e.printStackTrace();
		return null;
	}
}
/**
 * Creates a new instance of the given class. It is
 * assumed that it is a subclass of CodeSnippet.
 */
Object createCodeSnippet(Class snippetClass) {
	Object object = null;
	try {
		object = snippetClass.newInstance();
	} catch (InstantiationException e) {
		e.printStackTrace();
		this.ide.sendResult(void.class, null);
		return null;
	} catch (IllegalAccessException e) {
		e.printStackTrace();
		this.ide.sendResult(void.class, null);
		return null;
	}
	return object;
}
/**
 * Whether this code snippet runner is currently running.
 */
public boolean isRunning() {
	return this.ide.isConnected();
}
/**
 * Starts a new CodeSnippetRunner that will serve code snippets from the IDE.
 * It waits for a connection on the given evaluation port number.
 * <p> 
 * Usage: java org.eclipse.jdt.tests.eval.target.CodeSnippetRunner -evalport <portNumber> [-options] [<mainClassName>] [<arguments>]
 * where options include:
 * -cscp <codeSnippetClasspath> the the classpath directory for the code snippet classes.
 * that are not defined in a "java.*" package.
 * -csbp <codeSnippetBootClasspath> the bootclasspath directory for the code snippet classes
 * that are defined in a "java.*" package.
 * <p>
 * The mainClassName and its arguments are optional: when not present only the server will start 
 * and run until the VM is shut down, when present the server will start, the main class will run
 * but the server will exit when the main class has finished running.
 */
public static void main(String[] args) {
	int length = args.length;
	if (length < 2 || !args[0].toLowerCase().equals("-evalport")) {
		printUsage();
		return;
	}
	int evalPort = Integer.parseInt(args[1]);
	String classPath = null;
	String bootPath = null;
	int mainClass = -1;
	for (int i = 2; i < length; i++) {
		String arg = args[i];
		if (arg.startsWith("-")) {
			if (arg.toLowerCase().equals("-cscp")) {
				if (++i < length) {
					classPath = args[i];
				} else {
					printUsage();
					return;
				}
			} else if (arg.toLowerCase().equals("-csbp")) {
				if (++i < length) {
					bootPath = args[i];
				} else {
					printUsage();
					return;
				}
			}
		} else {
			mainClass = i;
			break;
		}
	}
	theRunner = new CodeSnippetRunner(evalPort, classPath, bootPath);
	if (mainClass == -1) {
		theRunner.start();
	} else {
		Thread server = new Thread() {
			public void run() {
				theRunner.start();
			}
		};
		server.setDaemon(true);
		server.start();
		int mainArgsLength = length-mainClass-1;
		String[] mainArgs = new String[mainArgsLength];
		System.arraycopy(args, mainClass+1, mainArgs, 0, mainArgsLength);
		try {
			Class clazz = Class.forName(args[mainClass]);
			Method mainMethod = clazz.getMethod("main", new Class[] {String[].class});
			mainMethod.invoke(null, new String[][] {mainArgs});
		} catch (ClassNotFoundException e) {
			e.printStackTrace();
		} catch (NoSuchMethodException e) {
			e.printStackTrace();
		} catch (IllegalAccessException e) {
			e.printStackTrace();
		} catch (InvocationTargetException e) {
			e.printStackTrace();
		}
	}
}
private static void printUsage() {
	System.out.println("Usage: java org.eclipse.jdt.tests.eval.target.CodeSnippetRunner -evalport <portNumber> [-options] [<mainClassName>] [<arguments>]");
	System.out.println("where options include:");
	System.out.println("-cscp <codeSnippetClasspath> the the classpath directory for the code snippet classes.");
	System.out.println("that are not defined in a \"java.*\" package.");
	System.out.println("-csbp <codeSnippetBootClasspath> the bootclasspath directory for the code snippet classes");
	System.out.println("that are defined in a \"java.*\" package.");
}
/**
 * Loads the given class definitions. The way these class definitions are loaded is described
 * in the CodeSnippetRunner constructor.
 * The class definitions are code snippet classes and/or global variable classes.
 * Code snippet classes are assumed be direct or indirect subclasses of CodeSnippet and implement 
 * only the run()V method. 
 * They are instanciated and run.
 * Global variable classes are assumed to be direct subclasses of CodeSnippet. Their fields are assumed
 * to be static. The value of each field is sent back to the IDE.
 */
void processClasses(boolean mustRun, byte[][] classDefinitions) {
	// store the class definitions (either in the code snippet class loader or on disk)
	String[] newClasses = new String[classDefinitions.length];
	for (int i = 0; i < classDefinitions.length; i++) {
		byte[] classDefinition = classDefinitions[i];
		String classFileName = className(classDefinition);
		String className = classFileName.replace('/', '.');
		if (this.loader != null) {
			this.loader.storeClassDefinition(className, classDefinition);
		} else {
			writeClassOnDisk(classFileName, classDefinition);
		}
		newClasses[i] = className;
	}

	// load the classes and collect code snippet classes
	Vector codeSnippetClasses = new Vector();
	for (int i = 0; i < newClasses.length; i++) {
		String className = newClasses[i];
		Class clazz = null;
		if (this.loader != null) {
			clazz = this.loader.loadIfNeeded(className);
			if (clazz == null) {
				System.err.println("Could not find class definition for " + className);
				break;
			}
		} else {
			// use the system class loader
			try {
				clazz = Class.forName(className);
			} catch (ClassNotFoundException e) {
				e.printStackTrace(); // should never happen since we just wrote it on disk
				this.ide.sendResult(void.class, null);
				break;
			}
		}
		
		Class superclass = clazz.getSuperclass();
		Method[] methods = clazz.getDeclaredMethods();
		if (this.codeSnippetClass == null) {
			if (superclass.equals(Object.class) && clazz.getName().equals(CODE_SNIPPET_CLASS_NAME)) {
				// The CodeSnippet class is being deployed
				this.codeSnippetClass = clazz;
			} else {
				System.out.println("Expecting CodeSnippet class to be deployed first");
			}
		} else if (superclass.equals(this.codeSnippetClass)) {
			// It may be a code snippet class with no global variable
			if (methods.length == 1 && methods[0].getName().equals(RUN_METHOD_NAME)) {
				codeSnippetClasses.addElement(clazz);
			}
			// Evaluate global variables and send result back
			Field[] fields = clazz.getDeclaredFields();
			for (int j = 0; j < fields.length; j++) {
				Field field = fields[j];
				if (Modifier.isPublic(field.getModifiers())) {
					try {
						this.ide.sendResult(field.getType(), field.get(null));
					} catch (IllegalAccessException e) {
						e.printStackTrace(); // Cannot happen because the field is public
						this.ide.sendResult(void.class, null);
						break;
					}
				}
			}
		} else if (this.codeSnippetClass.equals(superclass.getSuperclass()) && methods.length == 1 && methods[0].getName().equals("run")) {
			// It is a code snippet class with a global variable superclass
			codeSnippetClasses.addElement(clazz);
		}
	}

	// run the code snippet classes
	if (codeSnippetClasses.size() != 0 && mustRun) {
		for (Enumeration e = codeSnippetClasses.elements(); e.hasMoreElements();) {
			Object codeSnippet = this.createCodeSnippet((Class) e.nextElement());
			if (codeSnippet != null) {
				this.runCodeSnippet(codeSnippet);
			}
		}
	}
}
/**
 * Runs the given code snippet in a new thread and send the result back to the IDE.
 */
void runCodeSnippet(final Object snippet) {
	Thread thread = new Thread() {
		public void run() {
			try {
				try {
					Method runMethod = codeSnippetClass.getMethod(RUN_METHOD_NAME, new Class[] {});
					runMethod.invoke(snippet, new Object[] {});
				} finally {
					Method getResultTypeMethod = codeSnippetClass.getMethod(GET_RESULT_TYPE_METHOD_NAME, new Class[] {});
					Class resultType = (Class)getResultTypeMethod.invoke(snippet, new Object[] {});
					Method getResultValueMethod = codeSnippetClass.getMethod(GET_RESULT_VALUE_METHOD_NAME, new Class[] {});
					Object resultValue = getResultValueMethod.invoke(snippet, new Object[] {});
					CodeSnippetRunner.this.ide.sendResult(resultType, resultValue);
				}
			} catch (NoSuchMethodException e) {
				e.printStackTrace();
			} catch (IllegalAccessException e) {
				e.printStackTrace();
			} catch (IllegalArgumentException e) {
				System.out.println("codeSnippetClass = " + codeSnippetClass.getName());
				System.out.println("snippet.class = " + snippet.getClass().getName());
				Class superclass = snippet.getClass().getSuperclass();
				System.out.println("snippet.superclass = " + (superclass == null ? "null" : superclass.getName()));
				e.printStackTrace();
			} catch (InvocationTargetException e) {
				e.getTargetException().printStackTrace();
			}
		}
	};
	thread.setDaemon(true);
	thread.start();
}
/**
 * Starts this code snippet runner in a different thread.
 */
public void start() {
	Thread thread = new Thread("Code snippet runner") {
		public void run() {
			try {
				ide.connect();
			} catch (IOException e) {
				e.printStackTrace();
			}
			while (ide.isConnected()) {
				try {
					processClasses(ide.getRunFlag(), ide.getNextClasses());
				} catch (Error e) {
					ide.sendResult(void.class, null);
					e.printStackTrace();
				} catch (RuntimeException e) {
					ide.sendResult(void.class, null);
					e.printStackTrace();
				}
			}
		}
	};
	thread.start();
}
/**
 * Stops this code snippet runner.
 */
public void stop() {
	this.ide.disconnect();
}
private int u1At(int position, byte[] bytes) {
	return bytes[position] & 0xFF;
}
private int u2At(int position, byte[] bytes) {
	return ((bytes[position++] & 0xFF) << 8) + (bytes[position] & 0xFF);
}
private char[] utf8At(int readOffset, int bytesAvailable, byte[] bytes) {
	int x, y, z;
	int length = bytesAvailable;
	char outputBuf[] = new char[bytesAvailable];
	int outputPos = 0;
	while (length != 0) {
		x = bytes[readOffset++] & 0xFF;
		length--;
		if ((0x80 & x) != 0) {
			y = bytes[readOffset++] & 0xFF;
			length--;
			if ((x & 0x20) != 0) {
				z = bytes[readOffset++] & 0xFF;
				length--;
				x = ((x & 0x1F) << 12) + ((y & 0x3F) << 6) + (z & 0x3F);
			} else {
				x = ((x & 0x1F) << 6) + (y & 0x3F);
			}
		}
		outputBuf[outputPos++] = (char) x;
	}

	if (outputPos != bytesAvailable) {
		System.arraycopy(outputBuf, 0, (outputBuf = new char[outputPos]), 0, outputPos);
	}
	return outputBuf;
}
/**
 * Writes the given class definition on disk. The give name is the forward slash separated
 * fully qualified name of the class.
 */
private void writeClassOnDisk(String className, byte[] classDefinition) {
	try {
		String fileName = className.replace('/', File.separatorChar) + ".class";
		File classFile = new File(
			(this.bootclassPathDirectory != null && 
			(className.startsWith("java") || className.replace('/', '.').equals(CODE_SNIPPET_CLASS_NAME))) ? 
				this.bootclassPathDirectory : 
				this.classPathDirectory, fileName);
		File parent = new File(classFile.getParent());
		parent.mkdirs();
		if (!parent.exists()) {
			throw new IOException("Could not create directory " + parent.getPath());
		}
		FileOutputStream out = null;
		try {
			out = new FileOutputStream(classFile);
			out.write(classDefinition);
		} finally {
			if (out != null) {
				out.close();
			}
		}
	} catch (IOException e) {
		e.printStackTrace();
	}
}
}
