/*******************************************************************************
 * Copyright (c) 2000, 2020 IBM Corporation and others.
 *
 * This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License 2.0
 * which accompanies this distribution, and is available at
 * https://www.eclipse.org/legal/epl-2.0/
 *
 * SPDX-License-Identifier: EPL-2.0
 *
 * Contributors:
 *     IBM Corporation - initial API and implementation
 *     Nina Rinskaya
 *     		Fix for https://bugs.eclipse.org/bugs/show_bug.cgi?id=172820.
 *******************************************************************************/
package org.eclipse.jdt.core.tests.runtime;

import java.io.*;
import java.util.*;

import org.eclipse.jdt.core.JavaCore;
import org.eclipse.jdt.core.tests.util.Util;

/**
 * The root of the VM launchers that launch VMs on the same machine.
 * <p>
 * A local VM launcher has the following limitations:
 * <ul>
 *   <li>It can only retrieve the running virtual machines that it has launched. So if
 *       a client is using 2 instances of <code>LocalVMLauncher</code>, each of these
 *       instances will be able to retrieve only a part of the running VMs.
 * </ul>
 */
public abstract class LocalVMLauncher implements RuntimeConstants {

	static final String[] env = System.getenv().entrySet().stream()
		.filter(e -> !"JAVA_TOOL_OPTIONS".equals(e.getKey()))
		.map(e -> e.getKey() + "=" + e.getValue())
		.toArray(String[]::new);

	/**
	 * Whether the target has a file system and thus whether it supports writing
	 * class files to disk. See org.eclipse.jdt.core.tests.eval.target.CodeSnippetRunner for more
	 * information.
	 */
	public static final boolean TARGET_HAS_FILE_SYSTEM = true;
	public static final String REGULAR_CLASSPATH_DIRECTORY = "regularPath";
	public static final String BOOT_CLASSPATH_DIRECTORY = "bootPath";

	protected String[] bootPath;
	protected String[] classPath;
	protected int debugPort = -1;
	protected int evalPort = -1;
	protected String evalTargetPath;
	protected String[] programArguments;
	protected String programClass;
	protected List<LocalVirtualMachine> runningVMs = new ArrayList<>(); // a Vector of LocalVirtualMachine
	protected String[] vmArguments;
	protected String vmPath;

/**
 * Returns a launcher that will launch the same kind of VM that is currently running
 */
public static LocalVMLauncher getLauncher() {
	final String vmName = System.getProperty("java.vm.name");
	if ("J9".equals(vmName)) {
		return new J9VMLauncher();
	}
	if (vmName != null && vmName.indexOf("JRockit") != -1) {
		return new JRockitVMLauncher();
	}
	final String osName = System.getProperty("os.name");
	if (osName.startsWith("Mac")) {
		return new MacVMLauncher();
	}
	String javaVersion = System.getProperty("java.version");
	boolean isJrt = javaVersion != null && javaVersion.length() > 0 && JavaCore.compareJavaVersions(javaVersion, "9") >= 0;
	File file = new File(Util.getJREDirectory() + (isJrt ?	"/lib/jrt-fs.jar" :  "/lib/rt.jar"));
	if (file.exists()) {
		return new StandardVMLauncher();
	}
	if ("IBM J9SE VM".equals(vmName)) {
		return new SideCarJ9VMLauncher();
	}
	if ("DRLVM".equals(vmName)) {
		return new DRLVMLauncher();
	}
	return new SideCarVMLauncher();
}
/**
 * Builds the actual class path that is going to be passed to the VM.
 */
protected String buildClassPath() {
	StringBuilder classPathString = new StringBuilder();
	char pathSeparator = File.pathSeparatorChar;

	// Add jar support if in evaluation mode
	if (this.evalPort != -1) {
		classPathString.append(new File(this.evalTargetPath, SUPPORT_ZIP_FILE_NAME).getPath());
		classPathString.append(pathSeparator);
	}

	// Add class path given by client
	if (this.classPath != null) {
		int length = this.classPath.length;
		for (int i = 0; i < length; i++){
			classPathString.append(this.classPath[i]);
			classPathString.append(pathSeparator);
		}
	}

	// Add regular classpath directory if needed
	if (this.evalPort != -1 && TARGET_HAS_FILE_SYSTEM) {
		classPathString.append(this.evalTargetPath);
		classPathString.append(File.separatorChar);
		classPathString.append(REGULAR_CLASSPATH_DIRECTORY);
	}

	return classPathString.toString();
}
/**
 * Launches the VM by exec'ing the command line and returns the resulting Process.
 */
protected Process execCommandLine() throws TargetException {
	// Check that the VM path has been specified
	if (this.vmPath == null) {
		throw new TargetException("Path to the VM has not been specified");
	}

	// Check that the program class has been specified if not in evaluation mode
	if ((this.programClass == null) && (this.evalPort == -1)) {
		throw new TargetException("Program class has not been specified");
	}

	// Launch VM
	Process vmProcess= null;
	try {
		// Use Runtime.exec(String[]) with tokens because Runtime.exec(String) with commandLineString
		// does not properly handle spaces in arguments on Unix/Linux platforms.
		String[] commandLine = getCommandLine();
		vmProcess= Runtime.getRuntime().exec(commandLine, env);
	} catch (IOException e) {
		throw new TargetException("Error launching VM at " + this.vmPath);
	}
	return vmProcess;
}
/**
 * Returns the boot class path used when a VM is launched.
 */
public String[] getBootClassPath() {
	return this.bootPath;
}
/**
 * Returns the class path used when a VM is launched.
 */
public String[] getClassPath() {
	return this.classPath;
}
/**
 * Returns the command line which will be used to launch the VM.
 * The segments are in the following order:
 * <p><ul>
 * <li> VM path,
 * <li> VM arguments,
 * <li> the class path,
 * <li> the program class
 * <li> the program arguments
 * </ul>
 */
public abstract String[] getCommandLine();
/**
 * Returns the debug port, or -1 if debug mode is disabled.
 * The default is -1.
 */
public int getDebugPort() {
	return this.debugPort;
}
/**
 * Returns the evaluation port for evaluation support.
 * The default is -1, indicating no evaluation support.
 *
 * @see #setEvalPort(int)
 */
public int getEvalPort() {
	return this.evalPort;
}
/**
 * Returns the evaluation target path for evaluation support.
 *
 * @see #setEvalTargetPath(String)
 */
public String getEvalTargetPath() {
	return this.evalTargetPath;
}
/**
 * Returns the arguments passed to the program class.
 * Returns null if the VM is being launched for evaluation support only.
 */
public String[] getProgramArguments() {
	if (this.evalPort != -1) {
		return null;
	}
	return this.programArguments;
}
/**
 * Returns the dot-separated, fully qualified name of the class to run.
 * It must implement main(String[] args).
 * Returns null if the VM is being launched for evaluation support only.
 */
public String getProgramClass() {
	if (this.evalPort != -1) {
		return null;
	}
	return this.programClass;
}
/**
 * Returns all the target VMs that are running at this launcher's target
 * address.
 * Note that these target VMs may or may not have been launched by this
 * launcher.
 * Note also that if the list of running VMs doesn't change on the target,
 * two calls to this method return VMs that are equal.
 *
 * @return the list of running target VMs
 */
public LocalVirtualMachine[] getRunningVirtualMachines() {
	// Select the VMs that are actually running
	List<LocalVirtualMachine> actuallyRunning = new ArrayList<>();
	for (LocalVirtualMachine vm : this.runningVMs) {
		if (vm.isRunning())
			actuallyRunning.add(vm);
	}
	this.runningVMs = actuallyRunning;

	// Return the running VMs
	int size = actuallyRunning.size();
	LocalVirtualMachine[] result = new LocalVirtualMachine[size];
	for (int i=0; i<size; i++)
		result[i] = actuallyRunning.get(i);
	return result;
}
/**
 * Returns the address of the target where this launcher runs the target VMs. The format
 * of this address is transport specific.
 * For example, a VM launcher using a TCP/IP transport returns target addresses looking like:
 * <code>"localhost:2010"</code>, or <code>"joe.ibm.com"</code>.
 *
 * @return transport specific address of the target
 */
public String getTargetAddress() {
	return "localhost";
}
/**
 * Returns the VM-specific arguments. This does not include:
 * <p><ul>
 * <li>the VM path
 * <li>the class path or the boot class path
 * <li>the program class or program arguments
 * </ul>
 */
public String[] getVMArguments() {
	return this.vmArguments;
}
/**
 * Returns the path on disk of the VM to launch.
 */
public String getVMPath() {
	return this.vmPath;
}
/**
 * Initializes this context's target path by copying the jar file for the code snippet support
 * and by creating the 2 directories that will contain the code snippet classes (see TARGET_HAS_FILE_SYSTEM).
 * Add the code snipport root class to the boot path directory so that code snippets can be run in
 * java.* packages
 *
 * @throws TargetException if the path could not be initialized with the code snippet support
 */
protected void initTargetPath() throws TargetException {
	// create directories
	File directory = new File(this.evalTargetPath);
	directory.mkdirs();
	if (!directory.exists()) {
		throw new TargetException("Could not create directory " + this.evalTargetPath);
	}
	if (TARGET_HAS_FILE_SYSTEM) {
		File classesDirectory = new File(directory, REGULAR_CLASSPATH_DIRECTORY);
		classesDirectory.mkdir();
		if (!classesDirectory.exists()) {
			throw new TargetException("Could not create directory " + classesDirectory.getPath());
		}
		File bootDirectory = new File(directory, BOOT_CLASSPATH_DIRECTORY);
		bootDirectory.mkdir();
		if (!bootDirectory.exists()) {
			throw new TargetException("Could not create directory " + bootDirectory.getPath());
		}
		/*
		// add the code snippet root class to the boot path directory
		InputStream in = null;
		try {
			in = EvaluationContext.class.getResourceAsStream("/" + SUPPORT_ZIP_FILE_NAME);
			ZipInputStream zip = new ZipInputStream(in);
			String rootClassFileName = ROOT_FULL_CLASS_NAME.replace('.', '/') + ".class";
			while (true) {
				ZipEntry entry = zip.getNextEntry();
				if (entry.getName().equals(rootClassFileName)) {
					// read root class file contents
					int size = (int)entry.getSize();
					byte[] buffer = new byte[size];
					int totalRead = 0;
					int read = 0;
					while (totalRead < size) {
						read = zip.read(buffer, totalRead, size - totalRead);
						if (read != -1) {
							totalRead += read;
						}
					}
					// write root class file contents
					FileOutputStream out = null;
					try {
						File rootFile = new File(bootDirectory, rootClassFileName.replace('/', File.separatorChar));
						File parent = new File(rootFile.getParent());
						parent.mkdirs();
						out = new FileOutputStream(rootFile);
						out.write(buffer);
						out.close();
					} catch (IOException e) {
						e.printStackTrace();
						if (out != null) {
							try {
								out.close();
							} catch (IOException e2) {
							}
						}
					}
					break;
				}
			}
			in.close();
		} catch (IOException e) {
			e.printStackTrace();
			if (in != null) {
				try {
					in.close();
				} catch (IOException e2) {
				}
			}
		}*/
	}

	// copy jar file
	InputStream in = null;
	FileOutputStream out = null;
	try {
		in = getClass().getResourceAsStream("/" + SUPPORT_ZIP_FILE_NAME);
		if (in == null) {
			throw new TargetException("Could not find resource /" + SUPPORT_ZIP_FILE_NAME);
		}
		int bufferLength = 1024;
		byte[] buffer = new byte[bufferLength];
		File file = new File(directory, SUPPORT_ZIP_FILE_NAME);
		out = new FileOutputStream(file);
		int read = 0;
		while (read != -1) {
			read = in.read(buffer, 0, bufferLength);
			if (read != -1) {
				out.write(buffer, 0, read);
			}
		}
	} catch (IOException e) {
		throw new TargetException("IOException while copying " + SUPPORT_ZIP_FILE_NAME + ": " + e.getMessage());
	} finally {
		if (in != null) {
			try {
				in.close();
			} catch (IOException e) {
			}
		}
		if (out != null) {
			try {
				out.close();
			} catch (IOException e) {
			}
		}
	}
}
/**
 * Launches a new target VM with the registered arguments.
 * This operation returns once a new target VM has been launched.
 *
 * @exception TargetException if the target VM could not be launched.
 */
public LocalVirtualMachine launch() throws TargetException {
	// evaluation mode
	if (this.evalTargetPath != null) {
		// init target path
		initTargetPath();
	}

	// launch VM
	LocalVirtualMachine vm;
	Process p = execCommandLine();
	vm = new LocalVirtualMachine(p, this.debugPort, this.evalTargetPath);

	// TBD: Start reading VM stdout and stderr right away otherwise this may prevent the connection
	//		from happening.

	// add VM to list of known running VMs
	this.runningVMs.add(vm);
	return vm;
}
/**
 * Sets the boot class path used when a VM is launched.
 */
public void setBootClassPath(java.lang.String[] bootClassPath) {
	this.bootPath = bootClassPath;
}
/**
 * Sets the class path used when a VM is launched.
 */
public void setClassPath(String[] classPath) {
	this.classPath = classPath;
}
/**
 * Sets the debug port to use for debug support.
 * Specify -1 to disable debug mode.
 */
public void setDebugPort(int debugPort) {
	this.debugPort = debugPort;
}
/**
 * Sets the evaluation port to use for evaluation support.
 * Setting the port enables evaluation support.
 * Specify null to disable evaluation support.
 */
public void setEvalPort(int evalPort) {
	this.evalPort = evalPort;
}
/**
 * Sets the evaluation target path to use for evaluation support.
 */
public void setEvalTargetPath(String evalTargetPath) {
	this.evalTargetPath = evalTargetPath;
}
/**
 * Sets the arguments passed to the program class.
 * This is ignored if the VM is being launched for evaluation support only.
 */
public void setProgramArguments(String[] args) {
	this.programArguments = args;
}
/**
 * Sets the dot-separated, fully qualified name of the class to run.
 * It must implement main(String[] args).
 * This is ignored if the VM is being launched for evaluation support only.
 */
public void setProgramClass(String programClass) {
	this.programClass = programClass;
}
/**
 * Sets the VM-specific arguments. This does not include:
 * <p><ul>
 * <li>the VM path
 * <li>the class path or the boot class path
 * <li>the program class or program arguments
 * </ul>
 */
public void setVMArguments(String[] args) {
	this.vmArguments = args;
}
/**
 * Sets the path on disk of the VM to launch.
 */
public void setVMPath(String vmPath) {
	this.vmPath = vmPath;
}
}
