package org.antlr.mojo.antlr3;

import java.util.List;
import java.util.Set;
import java.util.HashSet;
import java.util.ArrayList;
import java.util.Collections;
import java.io.File;
import java.io.IOException;
import java.io.Writer;
import java.io.FileWriter;
import java.io.BufferedWriter;
import java.net.URL;
import java.net.MalformedURLException;
import java.net.URLClassLoader;

import org.apache.maven.plugin.AbstractMojo;
import org.apache.maven.plugin.MojoExecutionException;
import org.apache.maven.plugin.MojoFailureException;
import org.apache.maven.project.MavenProject;
import org.apache.maven.artifact.Artifact;
import org.apache.maven.artifact.DependencyResolutionRequiredException;
import org.apache.maven.artifact.versioning.ArtifactVersion;
import org.apache.maven.artifact.versioning.DefaultArtifactVersion;
import org.apache.maven.artifact.versioning.OverConstrainedVersionException;
import org.codehaus.plexus.util.StringUtils;
import org.codehaus.plexus.util.FileUtils;
import org.codehaus.plexus.compiler.util.scan.mapping.SourceMapping;
import org.codehaus.plexus.compiler.util.scan.mapping.SuffixMapping;
import org.codehaus.plexus.compiler.util.scan.SourceInclusionScanner;
import org.codehaus.plexus.compiler.util.scan.SimpleSourceInclusionScanner;
import org.codehaus.plexus.compiler.util.scan.InclusionScanException;
import org.antlr.runtime.ANTLRFileStream;
import org.antlr.runtime.RecognitionException;
import org.antlr.gunit.GrammarInfo;
import org.antlr.gunit.gUnitExecutor;
import org.antlr.gunit.AbstractTest;
import org.antlr.gunit.Interp;
import org.apache.maven.plugin.logging.Log;
import org.apache.maven.plugins.annotations.Component;
import org.apache.maven.plugins.annotations.LifecyclePhase;
import org.apache.maven.plugins.annotations.Mojo;
import org.apache.maven.plugins.annotations.Parameter;
import org.apache.maven.plugins.annotations.ResolutionScope;

/**
 * Takes gUnit scripts and directly performs testing.
 * @author Steve Ebersole
 */
@Mojo(
		name = "gunit",
		defaultPhase = LifecyclePhase.TEST,
		requiresDependencyResolution = ResolutionScope.TEST,
		requiresProject = true)
public class GUnitExecuteMojo extends AbstractMojo {
	public static final String ANTLR_GROUP_ID = "org.antlr";
	public static final String ANTLR_ARTIFACT_NAME = "antlr";
	public static final String ANTLR_RUNTIME_ARTIFACT_NAME = "antlr-runtime";

	/**
     * INTERNAL : The Maven Project to which we are attached
     */
	@Parameter(defaultValue = "${project}", required = true)
    private MavenProject project;

	/**
	 * INTERNAL : The artifacts associated to the dependencies defined as part
	 * of our configuration within the project to which we are being attached.
	 */
	@Parameter(defaultValue = "${plugin.artifacts}", required = true, readonly = true)
	private List<Artifact> pluginArtifacts;

	/**
     * Specifies the directory containing the gUnit testing files.
     */
	@Parameter(defaultValue = "${basedir}/src/test/gunit", required = true)
    private File sourceDirectory;

    /**
     * A set of patterns for matching files from the sourceDirectory that
     * should be included as gUnit source files.
     */
	@Parameter
    private Set<String> includes;

    /**
     * A set of exclude patterns.
     */
	@Parameter
    private Set<String> excludes;

	/**
     * Specifies directory to which gUnit reports should get written.
     */
	@Parameter(defaultValue = "${basedir}/target/gunit-report", required = true)
	private File reportDirectory;

	/**
	 * Should gUnit functionality be completely by-passed?
	 * <p>
	 * By default we skip gUnit tests if the user requested that all testing be skipped using 'maven.test.skip'</p>
	 */
	@Parameter(defaultValue = "${maven.test.skip}")
	private boolean skip;

	public Set<String> getIncludePatterns() {
		return includes == null || includes.isEmpty()
				? Collections.singleton( "**/*.testsuite" )
				: includes;
	}

	public Set<String> getExcludePatterns() {
		return excludes == null
				? Collections.<String>emptySet()
				: excludes;
	}


	public final void execute() throws MojoExecutionException, MojoFailureException {
		if ( skip ) {
			getLog().info( "Skipping gUnit processing" );
			return;
		}
		Artifact pluginAntlrArtifact = determinePluginAntlrArtifact();

		validateProjectsAntlrVersion( determineArtifactVersion( pluginAntlrArtifact ) );

		performExecution( determineProjectCompileScopeClassLoader( pluginAntlrArtifact ) );
	}

	private Artifact determinePluginAntlrArtifact() throws MojoExecutionException {
		for ( Artifact artifact : pluginArtifacts ) {
			boolean match = ANTLR_GROUP_ID.equals( artifact.getGroupId() )
					&& ANTLR_ARTIFACT_NAME.equals( artifact.getArtifactId() );
			if ( match ) {
				return artifact;
			}
		}
		throw new MojoExecutionException(
				"Unexpected state : could not locate " + ANTLR_GROUP_ID + ':' + ANTLR_ARTIFACT_NAME +
						" in plugin dependencies"
		);
	}

	private ArtifactVersion determineArtifactVersion(Artifact artifact) throws MojoExecutionException {
		try {
			return artifact.getVersion() != null
					? new DefaultArtifactVersion( artifact.getVersion() )
					: artifact.getSelectedVersion();
		}
		catch ( OverConstrainedVersionException e ) {
			throw new MojoExecutionException( "artifact [" + artifact.getId() + "] defined an overly constrained version range" );
		}
	}

	private void validateProjectsAntlrVersion(ArtifactVersion pluginAntlrVersion) throws MojoExecutionException {
		Artifact antlrArtifact = null;
		Artifact antlrRuntimeArtifact = null;

		if ( project.getCompileArtifacts() != null ) {
			for ( Object o : project.getCompileArtifacts() ) {
				final Artifact artifact = ( Artifact ) o;
				if ( ANTLR_GROUP_ID.equals( artifact.getGroupId() ) ) {
					if ( ANTLR_ARTIFACT_NAME.equals( artifact.getArtifactId() ) ) {
						antlrArtifact = artifact;
						break;
					}
					if ( ANTLR_RUNTIME_ARTIFACT_NAME.equals( artifact.getArtifactId() ) ) {
						antlrRuntimeArtifact = artifact;
					}
				}
			}
		}

		validateBuildTimeArtifact( antlrArtifact, pluginAntlrVersion );
		validateRunTimeArtifact( antlrRuntimeArtifact, pluginAntlrVersion );
	}

	@SuppressWarnings(value = "unchecked")
	protected void validateBuildTimeArtifact(Artifact antlrArtifact, ArtifactVersion pluginAntlrVersion)
			throws MojoExecutionException {
		if ( antlrArtifact == null ) {
			validateMissingBuildtimeArtifact();
			return;
		}

		// otherwise, lets make sure they match...
		ArtifactVersion projectAntlrVersion = determineArtifactVersion( antlrArtifact );
		if ( pluginAntlrVersion.compareTo( projectAntlrVersion ) != 0 ) {
			getLog().warn(
					"Encountered " + ANTLR_GROUP_ID + ':' + ANTLR_ARTIFACT_NAME + ':' + projectAntlrVersion.toString() +
							" which did not match Antlr version used by plugin [" + pluginAntlrVersion.toString() + "]"
			);
		}
	}

	protected void validateMissingBuildtimeArtifact() {
		// generally speaking, its ok for the project to not define a dep on the build-time artifact...
	}

	@SuppressWarnings(value = "unchecked")
	protected void validateRunTimeArtifact(Artifact antlrRuntimeArtifact, ArtifactVersion pluginAntlrVersion)
			throws MojoExecutionException {
		if ( antlrRuntimeArtifact == null ) {
			// its possible, if the project instead depends on the build-time (or full) artifact.
			return;
		}

		ArtifactVersion projectAntlrVersion = determineArtifactVersion( antlrRuntimeArtifact );
		if ( pluginAntlrVersion.compareTo( projectAntlrVersion ) != 0 ) {
			getLog().warn(
					"Encountered " + ANTLR_GROUP_ID + ':' + ANTLR_RUNTIME_ARTIFACT_NAME + ':' + projectAntlrVersion.toString() +
							" which did not match Antlr version used by plugin [" + pluginAntlrVersion.toString() + "]"
			);
		}
	}

	/**
	 * Builds the classloader to pass to gUnit.
	 *
	 * @param antlrArtifact The plugin's (our) Antlr dependency artifact.
	 *
	 * @return The classloader for gUnit to use
	 *
	 * @throws MojoExecutionException Problem resolving artifacts to {@link java.net.URL urls}.
	 */
	private ClassLoader determineProjectCompileScopeClassLoader(Artifact antlrArtifact)
			throws MojoExecutionException {
		ArrayList<URL> classPathUrls = new ArrayList<URL>();
		getLog().info( "Adding Antlr artifact : " + antlrArtifact.getId() );
		classPathUrls.add( resolveLocalURL( antlrArtifact ) );

		for ( String path : classpathElements() ) {
			try {
				getLog().info( "Adding project compile classpath element : " + path );
				classPathUrls.add( new File( path ).toURI().toURL() );
			}
			catch ( MalformedURLException e ) {
				throw new MojoExecutionException( "Unable to build path URL [" + path + "]" );
			}
		}

		return new URLClassLoader( classPathUrls.toArray( new URL[classPathUrls.size()] ), getClass().getClassLoader() );
	}

	protected static URL resolveLocalURL(Artifact artifact) throws MojoExecutionException {
		try {
			return artifact.getFile().toURI().toURL();
		}
		catch ( MalformedURLException e ) {
			throw new MojoExecutionException( "Unable to resolve artifact url : " + artifact.getId(), e );
		}
	}

	@SuppressWarnings( "unchecked" )
	private List<String> classpathElements() throws MojoExecutionException {
		try {
			// todo : should we combine both compile and test scoped elements?
			return ( List<String> ) project.getTestClasspathElements();
		}
		catch ( DependencyResolutionRequiredException e ) {
			throw new MojoExecutionException( "Call to Project#getCompileClasspathElements required dependency resolution" );
		}
	}

	private void performExecution(ClassLoader projectCompileScopeClassLoader) throws MojoExecutionException {
		getLog().info( "gUnit report directory : " + reportDirectory.getAbsolutePath() );
		if ( !reportDirectory.exists() ) {
			boolean directoryCreated = reportDirectory.mkdirs();
			if ( !directoryCreated ) {
				getLog().warn( "mkdirs() reported problem creating report directory" );
			}
		}

		Result runningResults = new Result();
		ArrayList<String> failureNames = new ArrayList<String>();

		System.out.println();
		System.out.println( "-----------------------------------------------------------" );
		System.out.println( " G U N I T   R E S U L T S" );
		System.out.println( "-----------------------------------------------------------" );

		for ( File script : collectIncludedSourceGrammars() ) {
			final String scriptPath = script.getAbsolutePath();
			System.out.println( "Executing script " + scriptPath );
			try {
				String scriptBaseName = StringUtils.chompLast( FileUtils.basename( script.getName() ), "." );

				ANTLRFileStream antlrStream = new ANTLRFileStream( scriptPath );
				GrammarInfo grammarInfo = Interp.parse( antlrStream );
				gUnitExecutor executor = new gUnitExecutor(
						grammarInfo,
						projectCompileScopeClassLoader,
						script.getParentFile().getAbsolutePath()
				);

				String report = executor.execTest();
				writeReportFile( new File( reportDirectory, scriptBaseName + ".txt" ), report );

				Result testResult = new Result();
				testResult.tests = executor.numOfTest;
				testResult.failures = executor.numOfFailure;
				testResult.invalids = executor.numOfInvalidInput;

				System.out.println( testResult.render() );

				runningResults.add( testResult );
				for ( AbstractTest test : executor.failures ) {
					failureNames.add( scriptBaseName + "#" + test.getHeader() );
				}
			}
			catch ( IOException e ) {
				throw new MojoExecutionException( "Could not open specified script file", e );
			}
			catch ( RecognitionException e ) {
				throw new MojoExecutionException( "Could not parse gUnit script", e );
			}
		}

		System.out.println();
		System.out.println( "Summary :" );
		if ( ! failureNames.isEmpty() ) {
			System.out.println( "  Found " + failureNames.size() + " failures" );
			for ( String name : failureNames ) {
				System.out.println( "    - " + name );
			}
		}
		System.out.println( runningResults.render() );
		System.out.println();

		if ( runningResults.failures > 0 ) {
			throw new MojoExecutionException( "Found gUnit test failures" );
		}

		if ( runningResults.invalids > 0 ) {
			throw new MojoExecutionException( "Found invalid gUnit tests" );
		}
	}

	private Set<File> collectIncludedSourceGrammars() throws MojoExecutionException {
		SourceMapping mapping = new SuffixMapping( "g", Collections.<String>emptySet() );
        SourceInclusionScanner scan = new SimpleSourceInclusionScanner( getIncludePatterns(), getExcludePatterns() );
        scan.addSourceMapping( mapping );
		try {
			return scan.getIncludedSources( sourceDirectory, null );
		}
		catch ( InclusionScanException e ) {
			throw new MojoExecutionException( "Error determining gUnit sources", e );
		}
	}

	private void writeReportFile(File reportFile, String results) {
		try {
			Writer writer = new FileWriter( reportFile );
			writer = new BufferedWriter( writer );
			try {
				writer.write( results );
				writer.flush();
			}
			finally {
				try {
					writer.close();
				}
				catch ( IOException ignore ) {
				}
			}
		}
		catch ( IOException e ) {
			getLog().warn(  "Error writing gUnit report file", e );
		}
	}

	private static class Result {
		private int tests = 0;
		private int failures = 0;
		private int invalids = 0;

		public String render() {
			return String.format( "Tests run: %d,  Failures: %d,  Invalid: %d", tests, failures, invalids );
		}

		public void add(Result result) {
			this.tests += result.tests;
			this.failures += result.failures;
			this.invalids += result.invalids;
		}
	}

}
