package org.bndtools.builder.validate;

import static aQute.bnd.osgi.Constants.DEFAULT_PROP_BIN_DIR;
import static aQute.bnd.osgi.Constants.DEFAULT_PROP_SRC_DIR;
import static aQute.bnd.osgi.Constants.DEFAULT_PROP_TESTBIN_DIR;
import static aQute.bnd.osgi.Constants.DEFAULT_PROP_TESTSRC_DIR;

import java.io.File;
import java.util.EnumSet;
import java.util.HashSet;
import java.util.Set;

import org.bndtools.api.BndtoolsConstants;
import org.bndtools.api.IProjectValidator;
import org.bndtools.api.IValidator;
import org.eclipse.core.resources.IFile;
import org.eclipse.core.resources.ResourcesPlugin;
import org.eclipse.core.runtime.IPath;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Path;
import org.eclipse.core.runtime.Status;
import org.eclipse.jdt.core.IClasspathEntry;
import org.eclipse.jdt.core.IJavaProject;

import aQute.bnd.build.Project;
import aQute.bnd.osgi.Builder;
import aQute.bnd.osgi.Constants;
import aQute.lib.io.IO;
import aQute.service.reporter.Reporter.SetLocation;
import bndtools.central.Central;

/**
 * Verify that the build path setup for Eclipse matches the actual settings in
 * bnd.
 */
public class ProjectPathsValidator implements IValidator, IProjectValidator {
	final static IPath JRE_CONTAINER = new Path("org.eclipse.jdt.launching.JRE_CONTAINER");

	/*
	 * The parts of the test, needed to know what we missed
	 */
	enum SetupTypes {
		testsrc,
		bndcontainer;
	}

	/*
	 * Old validate method for backward compatibility (did not want to create
	 * yet another extension point for this).
	 */
	@Override
	public IStatus validate(Builder builder) {
		return Status.OK_STATUS;
	}

	/**
	 * Validate the project
	 */
	@Override
	public void validateProject(Project model) throws Exception {
		//
		// We must have, a project and assume this is already reported
		//
		if (model == null) {
			return;
		}

		IJavaProject javaProject = Central.getJavaProject(model);
		if (javaProject == null) {
			model.error("Bndtools: The project in %s is not linked with a Java project.", model.getBase());
			return;
		}

		//
		// Verify if we have the right relation to the cnf folder ...
		//
		Project w;
		try {
			w = Central.getWorkspace()
				.getProjectFromFile(model.getBase());
		} catch (Exception e) {
			w = null;
		}
		if (w == null || w != model) {
			model.error(
				"Bndtools: Error in setup, likely the cnf folder is not ../cnf relative to the project folder '%s'. The workspace is in '%s'.",
				model.getBase(), model.getWorkspace()
					.getBase());
			return;
		}

		//
		// Get the different bnd directories ...
		//
		File bin = model.getOutput();
		File testsrc = model.getTestSrc();
		IO.mkdirs(testsrc);
		File testbin = model.getTestOutput();
		Set<File> sourcePath = new HashSet<>(model.getSourcePath());
		for (File f : sourcePath)
			IO.mkdirs(f);

		//
		// All the things we should find when we have traversed the build path
		//
		Set<SetupTypes> found = EnumSet.allOf(SetupTypes.class);

		for (IClasspathEntry cpe : javaProject.getRawClasspath()) {
			int kind = cpe.getEntryKind();
			switch (kind) {
				case IClasspathEntry.CPE_VARIABLE :
					warning(model, null, null, cpe,
						"Bndtools: Found a variable in the eclipse build path, this variable is not available during continuous integration",
						cpe).file(new File(model.getBase(), ".classpath").getAbsolutePath());
					break;

				case IClasspathEntry.CPE_LIBRARY :
					warning(model, null, null, cpe,
						"Bndtools: The .classpath contains a library that will not be available during continuous integration: %s",
						cpe.getPath()).file(new File(model.getBase(), ".classpath").getAbsolutePath());
					break;

				case IClasspathEntry.CPE_CONTAINER :
					if (BndtoolsConstants.BND_CLASSPATH_ID.segment(0)
						.equals(cpe.getPath()
							.segment(0)))
						found.remove(SetupTypes.bndcontainer);
					else {
						IPath path = cpe.getPath();
						if (JRE_CONTAINER.segment(0)
							.equals(path.segment(0))) {
							if (path.segmentCount() == 1) {
								// warning because default might vary
								// <classpathentry kind="con"
								// path="org.eclipse.jdt.launching.JRE_CONTAINER"/>
								warning(model, null, path.toString(), cpe,
									"Bndtools: The .classpath contains a default JRE container: %s. This makes it undefined to what version you compile and this might differ between Continuous Integration and Eclipse",
									path).file(new File(model.getBase(), ".classpath").getAbsolutePath());
							} else {
								String segment = path.segment(1);
								if ("org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType".equals(segment)
									&& path.segmentCount() == 3) {
									// check javac version <classpathentry
									// kind="con"
									// path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-1.8"/>

									String javac = model.getProperty(Constants.JAVAC_SOURCE, "1.8");
									if (!path.segment(2)
										.endsWith(javac)) {
										warning(model, null, path.toString(), cpe,
											"Bndtools: The .JRE container is set to %s but bnd is compiling against %s",
											path.segment(2), javac)
												.file(new File(model.getBase(), ".classpath").getAbsolutePath());
									}
								} else {
									// warning because local/machine specific
									// <classpathentry kind="con"
									// path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.launching.macosx.MacOSXType/Java
									// SE 7 [1.7.0_71]"/>
									warning(model, null, path.toString(), cpe,
										"Bndtools: The .classpath contains an non-portable JRE container: %s. This makes it undefined to what version you compile and this might differ between Continuous Integration and Eclipse",
										path).file(new File(model.getBase(), ".classpath").getAbsolutePath());
								}
							}
						} else {
							warning(model, null, path.toString(), cpe,
								"Bndtools: The .classpath contains an unknown container: %s. This could make your build less portable.",
								path).file(new File(model.getBase(), ".classpath").getAbsolutePath());
						}
					}
					break;

				case IClasspathEntry.CPE_SOURCE :
					File file = toFile(cpe.getPath());
					if (file == null) {
						model.warning("Bndtools: Found virtual file for '%s'", cpe.getPath())
							.details(cpe);
					} else {
						File output = toFile(cpe.getOutputLocation());
						if (output == null)
							output = toFile(javaProject.getOutputLocation());

						if (file.equals(testsrc)) {
							//
							// We're talking about the test source directory
							// This should be linked to testbin
							//
							found.remove(SetupTypes.testsrc);
							if (!testbin.equals(output)) {
								warning(model, DEFAULT_PROP_TESTBIN_DIR, testbin, cpe,
									"Bndtools: testsrc folder '%s' has output folder set to '%s', which does not match bnd's testbin folder '%s'",
									file, output, testbin);
							}
						} else {
							//
							// We must have a source directory. They must be
							// linked to the bin folder and on the bnd source
							// path. Since the source path has potentially
							// multiple entries, we remove this one so we can
							// check later if we had all of them
							if (sourcePath.remove(file)) {
								if (!bin.equals(output)) {
									warning(model, DEFAULT_PROP_BIN_DIR, bin, cpe,
										"Bndtools: src folder '%s' has output folder set to '%s', which does not match bnd's bin folder '%s'",
										file, output, bin);
								}
							} else {
								warning(model, DEFAULT_PROP_SRC_DIR, null, cpe,
									"Bndtools: Found source folder '%s' that is not on bnd's source path '%s'", file,
									model.getProperty(Constants.DEFAULT_PROP_SRC_DIR));
							}
						}
					}
					break;

				default :
					break;
			}

		}

		//
		// If we had not see all source entries, then we should
		// have something in sourcePath
		//
		for (File file : sourcePath) {
			warning(model, DEFAULT_PROP_SRC_DIR, file, null,
				"Bndtools: bnd's src folder '%s' is not in the Eclipse build path", file);
		}

		//
		// Check if we had all the different things we needed to check
		//
		for (SetupTypes t : found) {
			switch (t) {
				case testsrc :
					// if the testsrc directory does not exist or has no
					// children, then don't warn
					File[] subs = testsrc.listFiles();
					if (subs != null && subs.length > 0)
						warning(model, DEFAULT_PROP_TESTSRC_DIR, null, null,
							"Bndtools: bnd's testsrc folder '%s' is not in the Eclipse build path", testsrc);
					break;

				case bndcontainer :
					warning(model, null, null, null,
						"Bndtools: The build path does not refer to the bnd container '%s'",
						BndtoolsConstants.BND_CLASSPATH_ID.segment(0));
					break;

				default :
					break;
			}
		}
	}

	private SetLocation warning(Project model, String header, Object context, IClasspathEntry cpe, String format,
		Object... args) {
		String prefix = model.getBase()
			.getAbsolutePath();
		for (int i = 0; i < args.length; i++) {
			args[i] = relative(prefix, args[i]);
		}
		SetLocation loc = model.warning(format, args);
		if (header != null)
			loc.header(header);
		if (context != null) {
			loc.context(relative(prefix, context).toString());
		}
		if (cpe != null) {
			loc.details(cpe);
		}
		loc.file(model.getPropertiesFile()
			.getAbsolutePath());
		return loc;
	}

	private Object relative(String prefix, Object object) {
		if (!(object instanceof File))
			return object;

		String path = object.toString();
		if (!path.startsWith(prefix))
			return object;

		return path.substring(prefix.length() + 1);
	}

	private File toFile(IPath path) {
		if (path == null)
			return null;

		try {
			IFile file = ResourcesPlugin.getWorkspace()
				.getRoot()
				.getFile(path);
			if (file != null)
				return file.getLocation()
					.toFile()
					.getAbsoluteFile();
		} catch (Exception e) {
			// ignore
		}
		return null;
	}
}
