/*
 * Hibernate Validator, declare and validate application constraints
 *
 * License: Apache License, Version 2.0
 * See the license.txt file in the root directory or <http://www.apache.org/licenses/LICENSE-2.0>.
 */
package org.hibernate.validator.ap.testutil;

import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.EnumSet;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

import javax.annotation.processing.Processor;
import javax.tools.Diagnostic;
import javax.tools.Diagnostic.Kind;
import javax.tools.DiagnosticCollector;
import javax.tools.JavaCompiler;
import javax.tools.JavaCompiler.CompilationTask;
import javax.tools.JavaFileObject;
import javax.tools.StandardJavaFileManager;
import javax.tools.StandardLocation;

import org.hibernate.validator.ap.internal.util.CollectionHelper;
import org.hibernate.validator.ap.internal.util.Configuration;
import org.hibernate.validator.ap.util.DiagnosticExpectation;

import static org.testng.Assert.assertEquals;
import static org.testng.Assert.fail;

/**
 * Infrastructure for unit tests based on the Java Compiler API.
 *
 * @author Gunnar Morling
 */
public class CompilerTestHelper {

	/**
	 * Dependencies which are used as class path elements for the compilation tasks.
	 *
	 * @author Gunnar Morling
	 */
	public enum Library {

		HIBERNATE_VALIDATOR( "hibernate-validator.jar" ),

		VALIDATION_API( "validation-api.jar" ),

		JODA_TIME( "joda-time.jar" ),

		JAVA_MONEY_API( "money-api.jar" );

		private final String name;

		private Library(String name) {
			this.name = name;
		}

		public String getName() {
			return name;
		}
	}

	private final JavaCompiler compiler;

	private final String sourceBaseDir;

	private final String testLibraryDir;

	private static final File BASE_DIR;
	private static final File TARGET_DIR;
	private static final File PROCESSOR_OUT_DIR;

	static {
		TARGET_DIR = getTargetDir();
		BASE_DIR = TARGET_DIR.getParentFile();
		PROCESSOR_OUT_DIR = new File( TARGET_DIR, "processor-generated-test-classes" );
		if ( !PROCESSOR_OUT_DIR.exists() ) {
			if ( !PROCESSOR_OUT_DIR.mkdirs() ) {
				fail( "Unable to create test output directory " + PROCESSOR_OUT_DIR.toString() );
			}
		}
	}

	public CompilerTestHelper(JavaCompiler compiler) {
		this.compiler = compiler;
		this.sourceBaseDir = BASE_DIR.getAbsolutePath() + "/src/test/java";
		this.testLibraryDir = TARGET_DIR.getAbsolutePath() + "/test-dependencies";
	}

	/**
	 * Retrieves a file object containing the source of the given class.
	 *
	 * @param clazz The class of interest.
	 *
	 * @return A file with the source of the given class.
	 */
	public File getSourceFile(Class<?> clazz) {
		String sourceFileName = File.separator + clazz.getName().replace( ".", File.separator ) + ".java";
		return new File( sourceBaseDir + sourceFileName );
	}

	/**
	 * @see CompilerTestHelper#compile(Processor, DiagnosticCollector, Kind, Boolean, Boolean, EnumSet, File...)
	 */
	public boolean compile(Processor annotationProcessor,
						   DiagnosticCollector<JavaFileObject> diagnostics,
						   File... sourceFiles) {
		return compile(
				annotationProcessor,
				diagnostics,
				null,
				null,
				null,
				EnumSet.allOf( Library.class ),
				sourceFiles
		);
	}

	/**
	 * @see CompilerTestHelper#compile(Processor, DiagnosticCollector, Kind, Boolean, Boolean, EnumSet, File...)
	 */
	public boolean compile(Processor annotationProcessor,
						   DiagnosticCollector<JavaFileObject> diagnostics,
						   boolean verbose,
						   boolean allowMethodConstraints,
						   File... sourceFiles) {
		return compile(
				annotationProcessor,
				diagnostics,
				null,
				verbose,
				allowMethodConstraints,
				EnumSet.allOf( Library.class ),
				sourceFiles
		);
	}

	/**
	 * @see CompilerTestHelper#compile(Processor, DiagnosticCollector, Kind, Boolean, Boolean, EnumSet, File...)
	 */
	public boolean compile(Processor annotationProcessor,
						   DiagnosticCollector<JavaFileObject> diagnostics,
						   EnumSet<Library> dependencies,
						   File... sourceFiles) {
		return compile( annotationProcessor, diagnostics, null, null, null, dependencies, sourceFiles );
	}


	/**
	 * Creates and executes a {@link CompilationTask} using the given input.
	 *
	 * @param annotationProcessor An annotation processor to be attached to the task.
	 * @param diagnostics An diagnostics listener to be attached to the task.
	 * @param diagnosticKind A value for the "diagnosticKind" option.
	 * @param verbose A value for the "verbose" option.
	 * @param allowMethodConstraints A value for the "methodConstraintsSupported" option.
	 * @param dependencies A set with libraries which shall be added to the class path of
	 * the compilation task.
	 * @param sourceFiles The source files to be compiled.
	 *
	 * @return True, if the source files could be compiled successfully (meaning
	 *         in especially, that the given annotation processor didn't raise
	 *         any errors), false otherwise.
	 */
	public boolean compile(Processor annotationProcessor,
						   DiagnosticCollector<JavaFileObject> diagnostics,
						   Kind diagnosticKind,
						   Boolean verbose,
						   Boolean allowMethodConstraints,
						   EnumSet<Library> dependencies,
						   File... sourceFiles) {
		StandardJavaFileManager fileManager = compiler.getStandardFileManager( null, null, null );
		Iterable<? extends JavaFileObject> compilationUnits = fileManager.getJavaFileObjects( sourceFiles );
		List<String> options = new ArrayList<String>();

		if ( diagnosticKind != null ) {
			options.add( String.format( "-A%s=%s", Configuration.DIAGNOSTIC_KIND_PROCESSOR_OPTION, diagnosticKind ) );
		}

		if ( verbose != null ) {
			options.add( String.format( "-A%s=%b", Configuration.VERBOSE_PROCESSOR_OPTION, verbose ) );
		}

		if ( allowMethodConstraints != null ) {
			options.add(
					String.format(
							"-A%s=%b",
							Configuration.METHOD_CONSTRAINTS_SUPPORTED_PROCESSOR_OPTION,
							allowMethodConstraints
					)
			);
		}

		try {
			fileManager.setLocation( StandardLocation.CLASS_PATH, getDependenciesAsFiles( dependencies ) );
			fileManager.setLocation( StandardLocation.CLASS_OUTPUT, Arrays.asList( PROCESSOR_OUT_DIR ) );
		}
		catch (IOException e) {
			throw new RuntimeException( e );
		}

		CompilationTask task = compiler.getTask( null, fileManager, diagnostics, options, null, compilationUnits );
		task.setProcessors( Arrays.asList( annotationProcessor ) );

		return task.call();
	}

	/**
	 * <p>
	 * Asserts, that the given diagnostics match with the given expectations.
	 * </p>
	 * <p>
	 * First checks, whether the number of actual diagnostics matches with the
	 * number of given expectations. If that's the case, {@link Kind} and line
	 * number of each expectation are compared.
	 * </p>
	 *
	 * @param diagnostics The actual diagnostics as populated by the executed
	 * {@link CompilationTask}.
	 * @param expectations The expectations to compare against.
	 */
	public static void assertThatDiagnosticsMatch(DiagnosticCollector<JavaFileObject> diagnostics, DiagnosticExpectation... expectations) {
		assertEquals( asExpectations( diagnostics.getDiagnostics() ), CollectionHelper.asTreeSet( expectations ) );
	}

	private static Set<DiagnosticExpectation> asExpectations(Collection<Diagnostic<? extends JavaFileObject>> diagnosticsList) {
		Set<DiagnosticExpectation> theValue = CollectionHelper.newTreeSet();

		for ( Diagnostic<? extends JavaFileObject> diagnostic : diagnosticsList ) {
			theValue.add( new DiagnosticExpectation( diagnostic.getKind(), diagnostic.getLineNumber() ) );
		}

		return theValue;
	}

	private Set<File> getDependenciesAsFiles(EnumSet<Library> dependencies) {
		Set<File> files = new HashSet<File>();

		for ( Library oneDependency : dependencies ) {
			files.add( new File( testLibraryDir + File.separator + oneDependency.getName() ) );
		}

		return files;
	}

	/**
	 * Returns the target directory of the build.
	 *
	 * @return the target directory of the build
	 */
	public static File getTargetDir() {
		// target/test-classes
		String targetClassesDir = CompilerTestHelper.class.getProtectionDomain().getCodeSource().getLocation().getFile();
		return new File( targetClassesDir ).getParentFile();
	}
}
