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

package com.sysdeo.eclipse.tomcat;

import java.io.File;
import java.io.IOException;
import java.io.PrintWriter;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;

import org.eclipse.core.resources.IFile;
import org.eclipse.core.resources.IProject;
import org.eclipse.core.resources.IProjectNature;
import org.eclipse.core.resources.IResource;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IPath;
import org.eclipse.core.runtime.Path;
import org.eclipse.debug.core.ILaunch;
import org.eclipse.jdt.core.IClasspathContainer;
import org.eclipse.jdt.core.IClasspathEntry;
import org.eclipse.jdt.core.IJavaProject;
import org.eclipse.jdt.core.JavaCore;
import org.eclipse.jdt.core.JavaModelException;
import org.eclipse.jdt.launching.IVMInstall;

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

/**
 * Start and stop Tomcat
 * Subclasses contains all information specific to a Tomcat Version
 */
public abstract class TomcatBootstrap {

	/** WEBAPP_CLASSPATH_FILENAME */
	private static final String WEBAPP_CLASSPATH_FILENAME = ".#webclasspath";
	/** RUN */
	private static final int RUN = 1;
	/** LOG */
	private static final int LOG = 2;
	/** ADD_LAUNCH */
	private static final int ADD_LAUNCH = 3;

	/** debugMode */
	private boolean debugMode;
	/** tomcatDir */
	private String tomcatDir = "";
	/** tomcatBase */
	private String tomcatBase = "";
	/** projectMap */
	private Map<IProject, IProjectNature> projects = Collections.emptyMap();
	/** classpath */
	private String[] classpath = new String[0];
	/** vmArgs */
	private String[] vmArgs = new String[0];
	/** bootClasspath */
	private String[] bootClasspath = new String[0];
	/** IVMInstall */
	private IVMInstall vmInstalled;
	/** ProjectListElement */
	private List<ProjectListElement> elements;

	/**
	 * set DebugMode
	 * @param val DebugMode
	 */
	public void setDebugMode(final boolean val) {
		this.debugMode = val;
	}

	/**
	 *
	 * @return boolean
	 */
	private boolean isDebugMode() {
		return this.debugMode;
	}

	/**
	 * @param val the tomcatDir to set
	 */
	public void setTomcatDir(final String val) {
		this.tomcatDir = val;
	}

	/**
	 *
	 * @return TomcatDir
	 */
	protected String getTomcatDir() {
		return this.tomcatDir;
	}

	/**
	 * @param val the tomcatBase to set
	 */
	public void setTomcatBase(final String val) {
		this.tomcatBase = val;
	}

	/**
	 *
	 * @return TomcatBase
	 */
	protected String getTomcatBase() {
		return this.tomcatBase;
	}

	/**
	 * @param val the projects to set
	 */
	public void setProjects(final Map<IProject, IProjectNature> val) {
		this.projects = val;
	}

	/**
	 * @param val the classpath to set
	 */
	public void setClasspath(final String[] val) {
		this.classpath = val.clone();
	}

	/**
	 * @param val the vmArgs to set
	 */
	public void setVmArgs(final String[] val) {
		this.vmArgs = val.clone();
	}

	/**
	 * @param val the bootClasspath to set
	 */
	public void setBootClasspath(final String[] val) {
		this.bootClasspath = val.clone();
	}

	/**
	 * @return the vmInstalled
	 */
	protected IVMInstall getVmInstalled() {
		return this.vmInstalled;
	}

	/**
	 * @param val the vmInstalled to set
	 */
	public void setVmInstalled(final IVMInstall val) {
		this.vmInstalled = val;
	}

	/**
	 * @param val the elements to set
	 */
	public void setElements(final List<ProjectListElement> val) {
		this.elements = val;
	}

	/**
	 *
	 * @return String[]
	 */
	public abstract String[] getClasspath();

	/**
	 *
	 * @return String[]
	 */
	public abstract String[] getVmArgs();

	/**
	 *
	 * @param command String
	 * @return String[]
	 */
	public abstract String[] getPrgArgs(String command);

	/**
	 *
	 * @return String
	 */
	public abstract String getStartCommand();

	/**
	 *
	 * @return String
	 */
	public abstract String getStopCommand();

	/**
	 *
	 * @return String
	 */
	public abstract String getMainClass();

	/**
	 *
	 * @return String
	 */
	public abstract String getLabel();

	/**
	 *
	 * @param workFolder String
	 * @return String
	 */
	public abstract String getContextWorkDir(String workFolder);

	/**
	 *
	 * @return IPath
	 */
	public abstract IPath getServletJarPath();

	/**
	 *
	 * @return IPath
	 */
	public abstract IPath getJasperJarPath();

	/**
	 *
	 * @return IPath
	 */
	public abstract IPath getJSPJarPath();

	/**
	 *
	 * @return IPath
	 */
	public abstract IPath getElJarPath();

	/**
	 *
	 * @return IPath
	 */
	public abstract IPath getAnnotationsJarPath();

	/**
	 *
	 * @param tomcatHomePath IPath
	 * @return Collection<IClasspathEntry>
	 */
	public Collection<IClasspathEntry> getTomcatJars(final IPath tomcatHomePath) {
		ArrayList<IClasspathEntry> jars = new ArrayList<>();

		if (getServletJarPath() != null) {
			jars.add(JavaCore.newVariableEntry(tomcatHomePath.append(getServletJarPath()), null, null));
		}

		if (getJasperJarPath() != null)	{
			jars.add(JavaCore.newVariableEntry(tomcatHomePath.append(getJasperJarPath()), null, null));
		}

		if (getJSPJarPath() != null)	{
			jars.add(JavaCore.newVariableEntry(tomcatHomePath.append(getJSPJarPath()), null, null));
		}

		return jars;
	}

	/**
	 * Return the tag that will be used to find where context definition should be added in server.xml
	 * @return String
	 */
	public abstract String getXMLTagAfterContextDefinition();

	/**
	 * See %TOMCAT_HOME%/bin/startup.bat
	 * @throws CoreException CoreException
	 */
	public void start() throws CoreException {
		runTomcatBootsrap(getStartCommand(), RUN, false);
	}

	/**
	 * See %TOMCAT_HOME%/bin/shutdown.bat
	 * @throws CoreException CoreException
	 */
	public void stop() throws CoreException {
		runTomcatBootsrap(getStopCommand(), RUN, false);
	}

	/**
	 * Simply stop and start
	 * @throws CoreException CoreException
	 */
	public void restart() throws CoreException {
		stop();

		// Hack, need more testings
		try {
			Thread.sleep(5000);
		} catch (final InterruptedException ex) {
			Thread.interrupted();
			ex.printStackTrace(System.err);
		}

		start();
	}

	/**
	 * Write tomcat launch configuration to .metadata/.log
	 * @throws CoreException CoreException
	 */
	public void logConfig() throws CoreException {
		runTomcatBootsrap(getStartCommand(), LOG, false);
	}

	/**
	 * Create an Eclipse launch configuration
	 * @throws CoreException CoreException
	 */
	public void addLaunch() throws CoreException {
		runTomcatBootsrap(getStartCommand(), ADD_LAUNCH, true);
	}

	/**
	 * Launch a new JVM running Tomcat Main class
	 * Set classpath, bootclasspath and environment variable
	 *
	 * @param tomcatBootOption String
	 * @param action int
	 * @param saveConfig boolean
	 * @throws CoreException CoreException
	 */
	private void runTomcatBootsrap(final String tomcatBootOption, final int action,
					final boolean saveConfig) throws CoreException {

		for (final Entry<IProject, IProjectNature> ent: this.projects.entrySet()) {
			IProject project = ent.getKey();
			IProjectNature setting = ent.getValue();
			IProjectNature nature = project.getNature(JavaCore.NATURE_ID);

			if (!ITomcatProject.class.isInstance(setting)
							|| !IJavaProject.class.isInstance(nature)) {
				continue;
			}

			ITomcatProject tomcatProject = (ITomcatProject) setting;
			IJavaProject javaProject = (IJavaProject) nature;

			ArrayList<String> al = new ArrayList<>();
			ArrayList<String> visitedProjects = new ArrayList<>();
			WebClassPathEntries entries = tomcatProject.getWebClassPathEntries();
			if (entries != null) {
				getClassPathEntries(javaProject, al, entries.getList(), visitedProjects);
				print(tomcatProject, project, al);
			}
		}

		String[] cp = StringUtil.concatUniq(this.classpath, getClasspath());
		String[] args = StringUtil.concat(getVmArgs(), this.vmArgs);

		String[] prgArgs = getPrgArgs(tomcatBootOption);
		StringBuilder programArguments = new StringBuilder();
		for (final String arg : prgArgs) {
			programArguments.append(" ").append(arg);
		}

		StringBuilder jvmArguments = new StringBuilder();
		for (final String arg : args) {
			jvmArguments.append(" ").append(arg);
		}

		if (action == RUN) {
			VMLauncherUtility launcher = new VMLauncherUtility(this.tomcatDir, this.tomcatBase,
							cp, this.bootClasspath, this.vmInstalled, this.elements);
			ILaunch launch = launcher.runVM(getLabel(), getMainClass(), jvmArguments.toString(),
							programArguments.toString(), isDebugMode(), saveConfig);
			VMLauncherUtility.setILaunch(launch);
		} else if (action == LOG) {
			VMLauncherUtility launcher = new VMLauncherUtility(this.tomcatDir, this.tomcatBase,
							cp, this.bootClasspath, this.vmInstalled, this.elements);
			launcher.log(getLabel(), getMainClass(), jvmArguments.toString(), programArguments.toString());
		} else if (action == ADD_LAUNCH) {
			VMLauncherUtility launcher = new VMLauncherUtility(this.tomcatDir, this.tomcatBase,
							cp, this.bootClasspath, this.vmInstalled, this.elements);
			launcher.createConfig(getLabel(), getMainClass(),
							jvmArguments.toString(), programArguments.toString(), true);
		}
	}

	/**
	 *
	 * @param tomcatProject TomcatProject
	 * @param project IProject
	 * @param al ArrayList<String>
	 */
	private void print(final ITomcatProject tomcatProject, final IProject project, final ArrayList<String> al) {
		IFile file = null;
		if (tomcatProject.getRootDirFolder() == null) {
			file = project.getFile(new Path(WEBAPP_CLASSPATH_FILENAME));
		} else {
			file = tomcatProject.getRootDirFolder().getFile(new Path(WEBAPP_CLASSPATH_FILENAME));
		}

		File cpFile = file.getLocation().makeAbsolute().toFile();
		if (cpFile.exists()) {
			if (!cpFile.delete()) {
				return;
			}
		}
		try {
			if (cpFile.createNewFile()) {
				try (PrintWriter pw = new PrintWriter(cpFile, StandardCharsets.UTF_8.name())) {
					for (final String str : al) {
						pw.println(str);
					}
				}
			}
		} catch (final IOException e) {
			e.printStackTrace();
		}
	}

	/**
	 *
	 * @param data ArrayList<String>
	 * @param entry IPath
	 */
	private void add(final ArrayList<String> data, final IPath entry) {
		IPath ent = entry;
		if (!ent.isAbsolute()) {
			ent = ent.makeAbsolute();
		}
		String tmp = ent.toFile().toString();
		if (!data.contains(tmp)) {
			data.add(tmp);
		}
	}

	/**
	 *
	 * @param data ArrayList<String>
	 * @param con IResource
	 */
	private void add(final ArrayList<String> data, final IResource con) {
		if (con != null) {
			add(data, con.getLocation());
		}
	}

	/**
	 *
	 * @param prj IJavaProject
	 * @param data ArrayList<String>
	 * @param selectedPaths List<String>
	 * @param visitedProjects ArrayList<String>
	 * @throws JavaModelException JavaModelException
	 */
	private void getClassPathEntries(final IJavaProject prj, final ArrayList<String> data,
					final List<String> selectedPaths, final ArrayList<String> visitedProjects)
							throws JavaModelException {
		IPath outputPath = prj.getOutputLocation();
		if (selectedPaths.contains(outputPath.toFile().toString().replace('\\', '/'))) {
			add(data, prj.getProject().getWorkspace().getRoot().findMember(outputPath));
		}

		IClasspathEntry[] entries = prj.getRawClasspath();

		if (entries != null) {
			getClassPathEntries(entries, prj, data, selectedPaths, visitedProjects, outputPath);
		}
	}

	/**
	 *
	 * @param entries IClasspathEntry[]
	 * @param prj IJavaProject
	 * @param data ArrayList<String>
	 * @param selectedPaths List<String>
	 * @param visitedProjects ArrayList<String>
	 * @param outputPath IPath
	 * @throws JavaModelException JavaModelException
	 */
	private void getClassPathEntries(final IClasspathEntry[] entries, final IJavaProject prj,
					final ArrayList<String> data, final List<String> selectedPaths,
					final ArrayList<String> visitedProjects, final IPath outputPath) throws JavaModelException {
		for (final IClasspathEntry entry : entries) {
			IPath path = entry.getPath();
			if (entry.getEntryKind() == IClasspathEntry.CPE_SOURCE) {
				path = entry.getOutputLocation();
				if (path == null) {
					continue;
				}
			}
			if (entry.getEntryKind() == IClasspathEntry.CPE_PROJECT) {
				String prjName = entry.getPath().lastSegment();
				if (!visitedProjects.contains(prjName)) {
					visitedProjects.add(prjName);
					getClassPathEntries(prj.getJavaModel().getJavaProject(prjName),
									data, selectedPaths, visitedProjects);
				}
				continue;
			} else if (!selectedPaths.contains(path.toFile().toString().replace('\\', '/'))) {
				if (entry.getEntryKind() == IClasspathEntry.CPE_CONTAINER
								&& !"org.eclipse.jdt.launching.JRE_CONTAINER".equals(entry.getPath().toString())) {
					classPathEntries(path, prj, data, selectedPaths, visitedProjects, outputPath);
				}
				continue;
			}

			IClasspathEntry[] tmpEntry = getTmpEntry(entry, path, prj);
			if (tmpEntry == null) {
				continue;
			}

			for (final IClasspathEntry tmp : tmpEntry) {
				if (entry.getEntryKind() == IClasspathEntry.CPE_LIBRARY) {
					IResource res = prj.getProject().getWorkspace().getRoot().findMember(tmp.getPath());
					if (res != null) {
						add(data, res);
					} else {
						add(data, tmp.getPath());
					}
				} else if (entry.getEntryKind() == IClasspathEntry.CPE_SOURCE) {
					IPath srcPath = entry.getOutputLocation();
					if (srcPath != null && !srcPath.equals(outputPath)) {
						add(data, prj.getProject().getWorkspace().getRoot().findMember(srcPath));
					}
				} else {
					if (tmp.getPath() != null) {
						add(data, tmp.getPath());
					}
				}
			}
		}
	}

	/**
	 *
	 * @param path IPath
	 * @param prj IJavaProject
	 * @param data ArrayList<String>
	 * @param selectedPaths List<String>
	 * @param visitedProjects ArrayList<String>
	 * @param outputPath IPath
	 * @throws JavaModelException JavaModelException
	 */
	private void classPathEntries(final IPath path, final IJavaProject prj, final ArrayList<String> data,
					final List<String> selectedPaths, final ArrayList<String> visitedProjects,
					final IPath outputPath) throws JavaModelException {
		// entires in container are only processed individually
		// if container itself is not selected
		IClasspathContainer container = JavaCore.getClasspathContainer(path, prj);
		if (container != null) {
			getClassPathEntries(container.getClasspathEntries(), prj, data,
							selectedPaths, visitedProjects, outputPath);
		}
	}

	/**
	 *
	 * @param entry IClasspathEntry
	 * @param path IPath
	 * @param prj IJavaProject
	 * @return IClasspathEntry[]
	 * @throws JavaModelException JavaModelException
	 */
	private IClasspathEntry[] getTmpEntry(final IClasspathEntry entry, final IPath path,
					final IJavaProject prj) throws JavaModelException {
		IClasspathEntry[] tmpEntry = null;
		if (entry.getEntryKind() == IClasspathEntry.CPE_CONTAINER) {
			tmpEntry = JavaCore.getClasspathContainer(path, prj).getClasspathEntries();
		} else {
			tmpEntry = new IClasspathEntry[1];
			tmpEntry[0] = JavaCore.getResolvedClasspathEntry(entry);
		}
		return tmpEntry;
	}
}
