/*
 * (c) Copyright Sysdeo SA 2001, 2002.
 * All Rights Reserved.
 */

package com.sysdeo.eclipse.tomcat;

import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

import org.eclipse.core.resources.IProject;
import org.eclipse.core.resources.IProjectNature;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IPath;
import org.eclipse.core.runtime.Path;
import org.eclipse.debug.core.DebugPlugin;
import org.eclipse.debug.core.ILaunch;
import org.eclipse.debug.core.ILaunchConfiguration;
import org.eclipse.debug.core.ILaunchConfigurationType;
import org.eclipse.debug.core.ILaunchConfigurationWorkingCopy;
import org.eclipse.debug.core.ILaunchManager;
import org.eclipse.debug.core.model.ISourceLocator;
import org.eclipse.debug.core.sourcelookup.ISourceContainer;
import org.eclipse.debug.core.sourcelookup.ISourceLookupDirector;
import org.eclipse.debug.core.sourcelookup.ISourcePathComputer;
import org.eclipse.debug.core.sourcelookup.containers.DefaultSourceContainer;
import org.eclipse.debug.ui.IDebugUIConstants;
import org.eclipse.jdt.core.IClasspathEntry;
import org.eclipse.jdt.core.IJavaProject;
import org.eclipse.jdt.core.IPackageFragmentRoot;
import org.eclipse.jdt.core.JavaCore;
import org.eclipse.jdt.core.JavaModelException;
import org.eclipse.jdt.internal.launching.JavaSourceLookupDirector;
import org.eclipse.jdt.launching.IJavaLaunchConfigurationConstants;
import org.eclipse.jdt.launching.IRuntimeClasspathEntry;
import org.eclipse.jdt.launching.IVMInstall;
import org.eclipse.jdt.launching.JavaRuntime;
import org.eclipse.jdt.launching.sourcelookup.containers.JavaProjectSourceContainer;
import org.eclipse.jdt.launching.sourcelookup.containers.PackageFragmentRootSourceContainer;

import com.sysdeo.eclipse.tomcat.editors.ProjectListElement;

/**
 * Utility class for launching a JVM in Eclipse and registering it to debugger
 *
 * It might exist better way to implements those operations,
 * or they might already exist in other form JDT
 */
public class VMLauncherUtility {

	/** ILAUNCH */
	private static ILaunch ilaunch = null;

	/** tomcatDir */
	private final String tomcatDir;
	/** tomcatBase */
	private final String tomcatBase;
	/** classpath */
	private final String[] classpath;
	/** bootClasspath */
	private final String[] bootClasspath;
	/** IVMInstall */
	private final IVMInstall vmInstalled;
	/** ProjectListElement */
	private final List<ProjectListElement> elements;

	/**
	 * Constructor
	 * @param dir tomcatDir
	 * @param base tomcatBase
	 * @param cp classpath
	 * @param boot bootClasspath
	 * @param vm vmInstalled
	 * @param list List<ProjectListElement>
	 */
	VMLauncherUtility(final String dir, final String base, final String[] cp,
					final String[] boot, final IVMInstall vm, final List<ProjectListElement> list) {
		this.tomcatDir = dir;
		this.tomcatBase = base;
		this.classpath = cp;
		this.bootClasspath = boot;
		this.vmInstalled = vm;
		this.elements = list;
	}

	/**
	 * getILaunch
	 * @return ILaunch
	 */
	public static ILaunch getILaunch() {
		return ilaunch;
	}

	/**
	 * @param val the ilaunch to set
	 */
	static void setILaunch(final ILaunch val) {
		ilaunch = val;
	}

	/**
	 *
	 * @param label String
	 * @param classToLaunch String
	 * @param vmArgs String
	 * @param prgArgs String
	 * @param debug boolean
	 * @param saveConfig boolean
	 * @return ILaunch
	 * @throws CoreException CoreException
	 */
	ILaunch runVM(final String label, final String classToLaunch, final String vmArgs, final String prgArgs,
					final boolean debug, final boolean saveConfig) throws CoreException {

		String mode = "";
		if (debug) {
			mode = ILaunchManager.DEBUG_MODE;
		} else {
			mode = ILaunchManager.RUN_MODE;
		}

		ILaunchConfigurationWorkingCopy config = createConfig(label, classToLaunch, vmArgs, prgArgs, saveConfig);
		return config.launch(mode, null);
	}

	/**
	 *
	 * @param label String
	 * @param classToLaunch String
	 * @param vmArgs String
	 * @param prgArgs String
	 * @throws CoreException CoreException
	 */
	void log(final String label, final String classToLaunch, final String vmArgs,
						final String prgArgs) throws CoreException {
		StringBuilder trace = new StringBuilder("\n-------- Sysdeo Tomcat Launcher settings --------");
		trace.append("\n-> Label : ").append(label);
		trace.append("\n-> ClassToLaunch : ").append(classToLaunch);
		trace.append("\n-> Classpath : ");
		for (final String path : this.classpath) {
			trace.append(" | ").append(path).append(" | ");
		}
		trace.append("\n-> BootClasspath : ");
		for (final String boot : this.bootClasspath) {
			trace.append(" | ").append(boot).append(" | ");
		}
		trace.append("\n-> Vmargs : ").append(vmArgs);
		trace.append("\n-> PrgArgs : ").append(prgArgs);
		trace.append("\n-> Source lookup : \n");

		createConfig(label, classToLaunch, vmArgs, prgArgs, false);
		getSourceLocator();
	}

	/**
	 *
	 * @param label String
	 * @param classToLaunch String
	 * @param vmArgs String
	 * @param prgArgs String
	 * @param saveConfig boolean
	 * @return ILaunchConfigurationWorkingCopy
	 * @throws CoreException CoreException
	 */
	ILaunchConfigurationWorkingCopy createConfig(final String label, final String classToLaunch,
					final String vmArgs, final String prgArgs,
					final boolean saveConfig) throws CoreException {

		ILaunchConfigurationType launchType = DebugPlugin.getDefault().getLaunchManager().getLaunchConfigurationType(
						"org.eclipse.jdt.launching.localJavaApplication");
		ILaunchConfigurationWorkingCopy config = launchType.newInstance(null, label);
		config.setAttribute(IDebugUIConstants.ATTR_PRIVATE, !saveConfig);
		config.setAttribute(ILaunchConfiguration.ATTR_SOURCE_LOCATOR_ID,
						"org.eclipse.jdt.launching.sourceLocator.JavaSourceLookupDirector");

		ISourceLookupDirector locator = (ISourceLookupDirector) getSourceLocator();
		config.setAttribute(ILaunchConfiguration.ATTR_SOURCE_LOCATOR_MEMENTO, locator.getMemento());

		IVMInstall vmInstall = this.vmInstalled;
		config.setAttribute(IJavaLaunchConfigurationConstants.ATTR_JRE_CONTAINER_PATH,
						vmInstall.getInstallLocation().getPath());
		config.setAttribute(IJavaLaunchConfigurationConstants.ATTR_DEFAULT_CLASSPATH, false);
		config.setAttribute(IJavaLaunchConfigurationConstants.ATTR_CLASSPATH,
						getClasspathMementos(this.classpath, this.bootClasspath));
		config.setAttribute(IJavaLaunchConfigurationConstants.ATTR_PROGRAM_ARGUMENTS, prgArgs);
		config.setAttribute(IJavaLaunchConfigurationConstants.ATTR_VM_ARGUMENTS, vmArgs);
		config.setAttribute(IJavaLaunchConfigurationConstants.ATTR_MAIN_TYPE_NAME, classToLaunch);

		String catalinaBase = this.tomcatBase;
		if (catalinaBase.isEmpty()) {
			catalinaBase = this.tomcatDir;
		}
		config.setAttribute(IJavaLaunchConfigurationConstants.ATTR_WORKING_DIRECTORY, catalinaBase);

		if (saveConfig) {
			getSourceLocator();
			config.doSave();
		}

		return config;
	}

	/**
	 *
	 * @param classpath String[]
	 * @param bootClasspath String[]
	 * @return ClasspathMementos
	 * @throws CoreException CoreException
	 */
	private static List<String> getClasspathMementos(
					final String[] classpath, final String[] bootClasspath) throws CoreException {
		ArrayList<String> classpathMementos = new ArrayList<>();
		for (final String path : classpath) {
			IRuntimeClasspathEntry cpEntry = JavaRuntime.newArchiveRuntimeClasspathEntry(new Path(path));
			cpEntry.setClasspathProperty(IRuntimeClasspathEntry.USER_CLASSES);
			classpathMementos.add(cpEntry.getMemento());
		}

		if (bootClasspath.length == 0) {
			IPath path = new Path(JavaRuntime.JRE_CONTAINER);
			IClasspathEntry cpEntry = JavaCore.newContainerEntry(path);
			// from org.eclipse.jdt.internal.debug.ui.actions.AddLibraryAction run()
			IRuntimeClasspathEntry rcpEntry = JavaRuntime.newRuntimeContainerClasspathEntry(
							cpEntry.getPath(), IRuntimeClasspathEntry.STANDARD_CLASSES);
			classpathMementos.add(rcpEntry.getMemento());

		} else {
			for (final String boot : bootClasspath) {
				IRuntimeClasspathEntry cpEntry =
								JavaRuntime.newArchiveRuntimeClasspathEntry(new Path(boot));
				cpEntry.setClasspathProperty(IRuntimeClasspathEntry.BOOTSTRAP_CLASSES);
				classpathMementos.add(cpEntry.getMemento());
			}
		}
		return classpathMementos;
	}

	/**
	 *
	 * @return SourceLocator
	 * @throws CoreException CoreException
	 */
	private ISourceLocator getSourceLocator() throws CoreException {
		ArrayList<IProjectNature> tempList = new ArrayList<>();
		StringBuilder traceBuffer = new StringBuilder();
		traceBuffer.append("Projects in source path :\n");

		for (final ProjectListElement element : this.elements) {
			IProject project = element.getProject();
			traceBuffer.append("Project " + project.getName());
			if ((project.isOpen()) && project.hasNature(JavaCore.NATURE_ID)) {
				tempList.add(project.getNature(JavaCore.NATURE_ID));
				traceBuffer.append(" added to tempList\n");
			}
		}

		ISourceLookupDirector sourceLocator = new JavaSourceLookupDirector();
		ISourcePathComputer computer = DebugPlugin.getDefault().getLaunchManager().getSourcePathComputer(
						"org.eclipse.jdt.launching.sourceLookup.javaSourcePathComputer");
		sourceLocator.setSourcePathComputer(computer);
		sourceLocator.setSourceContainers(getSourceContainers(tempList, traceBuffer));
		sourceLocator.initializeParticipants();

		return sourceLocator;
	}

	/**
	 *
	 * @param tempList List<IProjectNature>
	 * @param traceBuffer traceBuffer
	 * @return SourceContainers
	 * @throws JavaModelException JavaModelException
	 */
	private static ISourceContainer[] getSourceContainers(final List<IProjectNature> tempList,
					final StringBuilder traceBuffer) throws JavaModelException {
		ArrayList<ISourceContainer> sourceContainers = new ArrayList<>();

		if (!tempList.isEmpty()) {
			IJavaProject[] javaProjects = tempList.toArray(new IJavaProject[1]);
			//		sourceLocator = new JavaSourceLocator(javaProjects, true);


			// Eclipse stops looking for source if it finds a jar containing the source code
			// despite this jar as no attached source (the user will have to use 'Attach source' button).
			// So we have to enforce that sources in project are searched before jar files,
			// To do so we add source containers in this orders :
			// - First project source containers.
			// - second packageFragmentRoot container (jar files in projects build path will be added to source path)
			// - third DefaultSourceContainer (jar files added to classpath will be added to source path)

			// First add all projects source containers
			for (final IJavaProject project : javaProjects) {
				traceBuffer.append("  -> Add JavaProjectSourceContainer for " + project.getProject().getName() + "\n");
				sourceContainers.add(new JavaProjectSourceContainer(project));
			}

			// Adding packageFragmentRoot source containers,
			// so classes in jar files associated to a project will be seen
			Set<IPath> external = new HashSet<>();
			for (final IJavaProject project : javaProjects) {
				traceBuffer.append("  -> Compute SourceContainers for " + project.getProject().getName() + " :\n");

				for (final IPackageFragmentRoot root : project.getPackageFragmentRoots()) {
					if (root.isExternal()) {
						IPath location = root.getPath();
						if (external.contains(location)) {
							continue;
						}
						external.add(location);
					}
					sourceContainers.add(new PackageFragmentRootSourceContainer(root));
					traceBuffer.append("     RootSourceContainer created for : "
									+ root.getPath().toPortableString() + "\n");
				}
			}
		}

		// Last add DefaultSourceContainer, classes in jar files added to classpath will be visible
		sourceContainers.add(new DefaultSourceContainer());

		return sourceContainers.toArray(new ISourceContainer[sourceContainers.size()]);
	}
}
