/*******************************************************************************
 * Copyright (c) 2005, 2006 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.cdt.core.testplugin;

// copied from startup.jar. planned to be removed soon


import java.io.File;
import java.io.FileFilter;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.ArrayList;
import java.util.List;
import java.util.StringTokenizer;
/**
 * Startup class for Eclipse. Creates a class loader using
 * supplied URL of platform installation, loads and calls
 * the Eclipse Boot Loader.  The startup arguments are as follows:
 * <dl>
 * <dd>
 *    -application &lt;id&gt;: the identifier of the application to run
 * </dd>
 * <dd>
 *    -boot &lt;location&gt;: the location, expressed as a URL, of the platform's boot.jar
 * </dd>
 * <dd>
 *    -consolelog : enables log to the console. Handy when combined with -debug
 * </dd>
 * <dd>
 *    -data &lt;location&gt;: sets the workspace location and the default location for projects
 * </dd>
 * <dd>
 *    -debug [options file]: turns on debug mode for the platform and optionally specifies a location
 * for the .options file. This file indicates what debug points are available for a
 * plug-in and whether or not they are enabled. If a location is not specified, the platform searches
 * for the .options file under the install directory
 * </dd>
 * <dd>
 *    -dev [entries]: turns on dev mode and optionally specifies comma-separated class path entries
 * which are added to the class path of each plug-in
 * </dd>
 * <dd>
 *    -keyring &lt;location&gt;: the location of the authorization database on disk. This argument
 * has to be used together with the -password argument
 * </dd>
 * <dd>
 *    -password &lt;passwd&gt;: the password for the authorization database
 * </dd>
 * <dd>
 *    -plugins &lt;location&gt;: The arg is a URL pointing to a file which specs the plugin 
 * path for the platform.  The file is in property file format where the keys are user-defined 
 * names and the values are comma separated lists of either explicit paths to plugin.xml 
 * files or directories containing plugins. (e.g., .../eclipse/plugins).
 * </dd>
 * <dd>
 *    -ws &lt;window system&gt;: sets the window system value
 * </dd>
 * </dl>
 */
public class Main {
	/**
	 * Indicates whether this instance is running in debug mode.
	 */
	protected boolean debug = false;
	
	/**
	 * The location of the launcher to run.
	 */
	protected String bootLocation = null;
	
	/**
	 * The identifier of the application to run.
	 */
	protected String application;
	
	/**
	 * The path for finding find plugins.
	 */
	protected URL pluginPathLocation;
	
	/**
	 * The boot path location.
	 */
	protected String location;
	
	/**
	 * Indicates whether items for UNinstallation should be looked for.
	 */
	protected boolean uninstall = false;
	
	/**
	 * The item to be uninstalled.
	 */
	protected String uninstallCookie;
	
	/**
	 * The class path entries.
	 */
	protected String devClassPath = null;
	
	/**
	 * Indicates whether this instance is running in development mode.
	 */
	protected boolean inDevelopmentMode = false;

	// static token describing how to take down the splash screen
	private static String endSplash = null;
	
	// constants
	private static final String APPLICATION = "-application";
	private static final String BOOT = "-boot";
	private static final String DEBUG = "-debug";
	private static final String DEV = "-dev";
	private static final String ENDSPLASH = "-endsplash";
	private static final String UNINSTALL = "-uninstall";
	private static final String PI_BOOT = "org.eclipse.core.boot";
	private static final String BOOTLOADER = "org.eclipse.core.boot.BootLoader";
	private static final String UPDATELOADER = "org.eclipse.core.internal.boot.LaunchInfo";

	// The project containing the boot loader code.  This is used to construct
	// the correct class path for running in VAJ and VAME.
	private static final String PROJECT_NAME = "Eclipse Core Boot";

	private static boolean inVAJ;
	static {
		try {
			Class.forName("com.ibm.uvm.lang.ProjectClassLoader");
			inVAJ = true;
		} catch (Exception e) {
			inVAJ = false;
		}
	}
	private static boolean inVAME;
	static {
		try {
			Class.forName("com.ibm.eclipse.core.VAME");
			inVAME = true;
		} catch (Exception e) {
			inVAME = false;
		}
	}

/**
 * Executes the launch.
 * 
 * @return the result of performing the launch
 * @param args command-line arguments
 * @exception Exception thrown if a problem occurs during the launch
 */
protected Object basicRun(String[] args) throws Exception {
	Class clazz = getBootLoader(bootLocation);
	Method method = clazz.getDeclaredMethod("run", new Class[] { String.class, URL.class, String.class, String[].class });
	try {
		return method.invoke(clazz, new Object[] { application, pluginPathLocation, location, args });
	} catch (InvocationTargetException e) {
		if (e.getTargetException() instanceof Error)
			throw (Error) e.getTargetException();
		else
			throw e;
	}
}

/**
 * Returns the result of converting a list of comma-separated tokens into an array
 * 
 * @return the array of string tokens
 * @param prop the initial comma-separated string
 */
private String[] getArrayFromList(String prop) {
	if (prop == null || prop.trim().equals(""))
		return new String[0];
	List<String> list = new ArrayList<String>();
	StringTokenizer tokens = new StringTokenizer(prop, ",");
	while (tokens.hasMoreTokens()) {
		String token = tokens.nextToken().trim();
		if (!token.equals(""))
			list.add(token);
	}
	return list.toArray(new String[list.size()]);
}
/**
 * Creates and returns a platform <code>BootLoader</code> which can be used to start
 * up and run the platform.  The given base, if not <code>null</code>,
 * is the location of the boot loader code.  If the value is <code>null</code>
 * then the boot loader is located relative to this class.
 * 
 * @return the new boot loader
 * @param base the location of the boot loader
 */
public Class getBootLoader(String base) throws Exception {
	URLClassLoader loader = new URLClassLoader(getBootPath(base), null);
	return loader.loadClass(BOOTLOADER);
}
/**
 * Returns the <code>URL</code>-based class path describing where the boot classes
 * are located when running in development mode.
 * 
 * @return the url-based class path
 * @param base the base location
 * @exception MalformedURLException if a problem occurs computing the class path
 */
protected URL[] getDevPath(URL base) throws MalformedURLException {
	URL url;
	String devBase = base.toExternalForm();
	if (!inDevelopmentMode) {
		url = new URL(devBase + "boot.jar");
		return new URL[] {url};
	}
	String[] locations = getArrayFromList(devClassPath);
	ArrayList result = new ArrayList(locations.length);
	for (int i = 0; i < locations.length; i++) {
		String spec = devBase + locations[i];
		char lastChar = spec.charAt(spec.length() - 1);
		if ((spec.endsWith(".jar") || (lastChar == '/' || lastChar == '\\')))
			url = new URL (spec);
		else
			url = new URL(spec + "/");
		//make sure URL exists before adding to path
		if (new java.io.File(url.getFile()).exists())
			result.add(url);
	}
	url = new URL(devBase + "boot.jar");
	if (new java.io.File(url.getFile()).exists())
		result.add(url);
	return (URL[])result.toArray(new URL[result.size()]);
}

/**
 * Returns the <code>URL</code>-based class path describing where the boot classes are located.
 * 
 * @return the url-based class path
 * @param base the base location
 * @exception MalformedURLException if a problem occurs computing the class path
 */
protected URL[] getBootPath(String base) throws MalformedURLException {
	URL url = null;
	// if the given location is not null, assume it is correct and use it. 
	if (base != null) {
		url = new URL(base);
		if (debug)
			System.out.println("Boot URL: " + url.toExternalForm());
		return new URL[] {url};	
	}
	// Create a URL based on the location of this class' code.
	// strip off jar file and/or last directory to get 
	// to the directory containing projects.
	URL[] result = null;
	url = getClass().getProtectionDomain().getCodeSource().getLocation();
	String path = url.getFile();
	if (path.endsWith(".jar"))
		path = path.substring(0, path.lastIndexOf("/"));
	else 
		if (path.endsWith("/"))
			path = path.substring(0, path.length() - 1);
	if (inVAJ || inVAME) {
		int ix = path.lastIndexOf("/");
		path = path.substring(0, ix + 1);
		path = path + PROJECT_NAME + "/";
		url = new URL(url.getProtocol(), url.getHost(), url.getPort(), path);
		result = new URL[] {url};
	} else {
		path = searchForPlugins(path);
		path = searchForBoot(path);
		// add on any dev path elements
		url = new URL(url.getProtocol(), url.getHost(), url.getPort(), path);
		result = getDevPath(url);
	}
	if (debug) {
		System.out.println("Boot URL:");
		for (int i = 0; i < result.length; i++)
			System.out.println("    " + result[i].toExternalForm());	
	}
	return result;
}

/**
 * Searches for a plugins root starting at a given location.  If one is
 * found then this location is returned; otherwise an empty string is
 * returned.
 * 
 * @return the location where plugins were found, or an empty string
 * @param start the location to begin searching at
 */
protected String searchForPlugins(String start) {
	File path = new File(start);
	while (path != null) {
		File test = new File(path, "plugins");
		if (test.exists())
			return test.toString();
		path = path.getParentFile();
		path = (path == null || path.length() == 1)  ? null : path;
	}
	return "";
}
/**
 * Searches for a boot directory starting at a given location.  If one
 * is found then this location is returned; otherwise an empty string
 * is returned.
 * 
 * @return the location where plugins were found, or an empty string
 * @param start the location to begin searching at
 */
protected String searchForBoot(String start) {
	FileFilter filter = new FileFilter() {
		@Override
		public boolean accept(File candidate) {
			return candidate.getName().startsWith(PI_BOOT);
		}
	};
	File[] boots = new File(start).listFiles(filter);
	String result = null;
	String maxVersion = null;
	for (int i = 0; i < boots.length; i++) {
		String name = boots[i].getName();
		int index = name.lastIndexOf('_');
		if (index == -1) {
			result = boots[i].getAbsolutePath();
			i = boots.length;
		} else {
			if (index > 0) {
				String version = name.substring(index + 1);
				if (maxVersion == null) {
					result = boots[i].getAbsolutePath();
					maxVersion = version;
				} else
					if (maxVersion.compareTo(version) == -1) {
						result = boots[i].getAbsolutePath();
						maxVersion = version;
					}						
			}
		}
	}
	if (result == null)
		throw new RuntimeException("Could not find bootstrap code. Check location of boot plug-in or specify -boot.");
	return result.replace(File.separatorChar, '/') + "/";
}
/**
 * Returns the update loader for the given boot path.
 * 
 * @return the update loader
 * @param base the boot path base
 * @exception Exception thrown is a problem occurs determining this loader
 */
public Class getUpdateLoader(String base) throws Exception {
	URLClassLoader loader = new URLClassLoader(getBootPath(base), null);
	return loader.loadClass(UPDATELOADER);
}
/**
 * Runs the platform with the given arguments.  The arguments must identify
 * an application to run (e.g., <code>-application com.example.application</code>).
 * After running the application <code>System.exit(N)</code> is executed.
 * The value of N is derived from the value returned from running the application.
 * If the application's return value is an <code>Integer</code>, N is this value.
 * In all other cases, N = 0.
 * <p>
 * Clients wishing to run the platform without a following <code>System.exit</code>
 * call should use <code>run()</code>.
 *
 * @see #run
 * 
 * @param args the command line arguments
 */
public static void main(String[] args) {
	Object result = null;
	try {
		result = new Main().run(args);
	} catch (Throwable e) {
		// try and take down the splash screen.
		endSplash();
		System.out.println("Exception launching the Eclipse Platform:");
		e.printStackTrace();
	}
	int exitCode = result instanceof Integer ? ((Integer) result).intValue() : 0;
	System.exit(exitCode);
}
/**
 * Tears down the currently-displayed splash screen.
 */
public static void endSplash() {
	if (endSplash == null)
		return;
	try {
		Runtime.getRuntime().exec(endSplash);
	} catch (Exception e) {
	}
}

/**
 * Runs this launcher with the arguments specified in the given string.
 * 
 * @param argString the arguments string
 * @exception Exception thrown if a problem occurs during launching
 */
public static void main(String argString) throws Exception {
	List<String> list = new ArrayList<String>(5);
	for (StringTokenizer tokens = new StringTokenizer(argString, " "); tokens.hasMoreElements();)
		list.add((String) tokens.nextElement());
	main(list.toArray(new String[list.size()]));
}

/**
 * Processes the command line arguments
 * 
 * @return the arguments to pass through to the launched application
 * @param args the command line arguments
 */
protected String[] processCommandLine(String[] args) throws Exception {
	int[] configArgs = new int[100];
	configArgs[0] = -1; // need to initialize the first element to something that could not be an index.
	int configArgIndex = 0;
	for (int i = 0; i < args.length; i++) {
		boolean found = false;
		// check for args without parameters (i.e., a flag arg)
		// check if debug should be enabled for the entire platform
		if (args[i].equalsIgnoreCase(DEBUG)) {
			debug = true;
			// passed thru this arg (i.e., do not set found = true
			continue;
		}
		
		// check if development mode should be enabled for the entire platform
		// If this is the last arg or there is a following arg (i.e., arg+1 has a leading -), 
		// simply enable development mode.  Otherwise, assume that that the following arg is
		// actually some additional development time class path entries.  This will be processed below.
		if (args[i].equalsIgnoreCase(DEV) && ((i + 1 == args.length) || ((i + 1 < args.length) && (args[i + 1].startsWith("-"))))) {
			inDevelopmentMode = true;
			// do not mark the arg as found so it will be passed through
			continue;
		}

		// done checking for args.  Remember where an arg was found 
		if (found) {
			configArgs[configArgIndex++] = i;
			continue;
		}
		// check for args with parameters. If we are at the last argument or if the next one
		// has a '-' as the first character, then we can't have an arg with a parm so continue.
		if (i == args.length - 1 || args[i + 1].startsWith("-")) 
			continue;
		String arg = args[++i];

		// look for the laucher to run
		if (args[i - 1].equalsIgnoreCase(BOOT)) {
			bootLocation = arg;
			found = true;
		}

		// look for the development mode and class path entries.  
		if (args[i - 1].equalsIgnoreCase(DEV)) {
			inDevelopmentMode = true;
			devClassPath = arg;
			continue;
		}

		// look for the application to run
		if (args[i - 1].equalsIgnoreCase(APPLICATION)) {
			application = arg;
			found = true;
		}

		// look for token to use to end the splash screen
		if (args[i - 1].equalsIgnoreCase(ENDSPLASH)) {
			endSplash = arg;
			continue;
		}

		// look for items to uninstall
		if (args[i - 1].equalsIgnoreCase(UNINSTALL)) {
			uninstall = true;
			uninstallCookie = arg;
			found = true;
		}

		// done checking for args.  Remember where an arg was found 
		if (found) {
			configArgs[configArgIndex++] = i - 1;
			configArgs[configArgIndex++] = i;
		}
	}
	// remove all the arguments consumed by this argument parsing
	if (configArgIndex == 0)
		return args;
	String[] passThruArgs = new String[args.length - configArgIndex];
	configArgIndex = 0;
	int j = 0;
	for (int i = 0; i < args.length; i++) {
		if (i == configArgs[configArgIndex])
			configArgIndex++;
		else
			passThruArgs[j++] = args[i];
	}
	return passThruArgs;
}
/**
 * Runs the application to be launched.
 * 
 * @return the return value from the launched application
 * @param args the arguments to pass to the application
 * @exception thrown if a problem occurs during launching
 */
public Object run(String[] args) throws Exception {
	String[] passThruArgs = processCommandLine(args);
	if (uninstall)
		return updateRun(UNINSTALL, uninstallCookie, passThruArgs);
	else
		return basicRun(passThruArgs);
}
/**
 * Performs an update run.
 * 
 * @return the return value from the update loader
 * @param flag flag to give to the update loader
 * @param value value to give to the update loader
 * @param args arguments to give to the update loader.
 * @exception Exception thrown if a problem occurs during execution
 */
protected Object updateRun(String flag, String value, String[] args) throws Exception {
	Class clazz = getUpdateLoader(bootLocation);
	Method method = clazz.getDeclaredMethod("run", new Class[] { String.class, String.class, String.class, String[].class });
	try {
		return method.invoke(clazz, new Object[] { flag, value, location, args });
	} catch (InvocationTargetException e) {
		if (e.getTargetException() instanceof Error)
			throw (Error) e.getTargetException();
		else
			throw e;
	}
}
}
