package org.codehaus.mojo.gwt.shell;

/*
 * CompileMojo.java
 *
 * Created on January 13, 2007, 11:42 AM
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
 * To change this template, choose Tools | Template Manager
 * and open the template in the editor.
 *
 */

import java.util.List;
import org.apache.maven.artifact.Artifact;
import org.apache.maven.plugin.MojoExecutionException;
import org.apache.maven.plugin.MojoFailureException;
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;
import org.codehaus.mojo.gwt.GwtModule;
import org.codehaus.mojo.gwt.utils.GwtModuleReaderException;
import org.codehaus.mojo.gwt.utils.ProjectScanner;
import org.codehaus.plexus.compiler.util.scan.InclusionScanException;
import org.codehaus.plexus.compiler.util.scan.StaleSourceScanner;
import org.codehaus.plexus.compiler.util.scan.mapping.SingleTargetSourceMapping;
import org.codehaus.plexus.util.StringUtils;

import java.io.File;
import java.io.IOException;
import java.io.PrintStream;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.SortedSet;
import java.util.TreeSet;

/**
 * Invokes the GWT Compiler for the project source.
 * See compiler options :
 * http://www.gwtproject.org/doc/latest/DevGuideCompilingAndDebugging.html#DevGuideCompilerOptions
 *
 * @version $Id$
 * @author cooper
 * @author ccollins
 * @author <a href="mailto:nicolas@apache.org">Nicolas De loof</a>
 * @author <a href="mailto:olamy@apache.org">Olivier Lamy</a>
 */
@Mojo(name = "compile", defaultPhase = LifecyclePhase.PREPARE_PACKAGE, requiresDependencyResolution = ResolutionScope.COMPILE, threadSafe = true)
public class CompileMojo
    extends AbstractGwtShellMojo
{

    @Parameter(property = "gwt.compiler.skip", defaultValue = "false")
    private boolean skip;

    /**
     * Don't try to detect if GWT compilation is up-to-date and can be skipped.
     * <p>
     * Can be set from command line using '-Dgwt.compiler.force=true'.
     */
    @Parameter(property = "gwt.compiler.force", defaultValue = "false")
    private boolean force;

    /**
     * On GWT 1.6+, number of parallel processes used to compile GWT premutations. Defaults to
     * platform available processors number.
     * 
     * <p>
     * Can be unset from command line using '-Dgwt.compiler.localWorkers=n'.
     * </p>
     */
    @Parameter(property = "gwt.compiler.localWorkers")
    private int localWorkers;

    /**
     * Whether or not to enable assertions in generated scripts (-checkAssertions).
     */
    @Parameter(alias = "enableAssertions", defaultValue = "false")
    private boolean checkAssertions;

    /**
     * EXPERIMENTAL: Disables some java.lang.Class methods (e.g. getName()).
     * <p>
     * Can be set from command line using '-Dgwt.disableClassMetadata=true'.
     * </p>
     */
    @Parameter(defaultValue = "false", property = "gwt.disableClassMetadata")
    private boolean disableClassMetadata;

    /**
     * EXPERIMENTAL: Disables run-time checking of cast operations.
     * <p>
     * Can be set from command line using '-Dgwt.disableCastChecking=true'.
     * </p>
     */
    @Parameter(defaultValue = "false", property = "gwt.disableCastChecking")
    private boolean disableCastChecking;

    /**
     * EXPERIMENTAL: Disables code-splitting.
     * <p>
     * Can be set from command line using '-Dgwt.disableRunAsync=true'.
     * </p>
     */
    @Parameter(defaultValue = "false", property = "gwt.disableRunAsync")
    private boolean disableRunAsync;

    /**
     * Validate all source code, but do not compile.
     * <p>
     * Can be set from command line using '-Dgwt.validateOnly=true'.
     * </p>
     */
    @Parameter(defaultValue = "false", property = "gwt.validateOnly")
    private boolean validateOnly;

    /**
     * Enable faster, but less-optimized, compilations.
     * <p>
     * Can be set from command line using '-Dgwt.draftCompile=true'.
     * </p>
     */
    @Parameter(defaultValue = "false", property = "gwt.draftCompile")
    private boolean draftCompile;

    /**
     * The directory into which extra, non-deployed files will be written.
     */
    @Parameter(defaultValue = "${project.build.directory}/extra")
    private File extra;

    /**
     * The compiler's working directory for internal use (must be writeable; defaults to a system temp dir)
     */
    @Parameter
    private File workDir;

    /**
     * add -extra parameter to the compiler command line
     * <p>
     * Can be set from command line using '-Dgwt.extraParam=true'.
     *
     * @since 2.1.0-1
     */
    @Parameter(defaultValue = "false", property = "gwt.extraParam")
    private boolean extraParam;

    /**
     * Compile a report that tells the "Story of Your Compile".
     * <p>
     * Can be set from command line using '-Dgwt.compiler.compileReport=true'.
     * </p>
     *
     * @since 2.1.0-1
     */
    @Parameter(defaultValue = "false", property = "gwt.compiler.compileReport")
    private boolean compileReport;

    /**
     * Sets the optimization level used by the compiler.  0=none 9=maximum.
     * <p>
     * -1 uses the default level of the compiler.
     * </p>
     * <p>
     * Can be set from command line using '-Dgwt.compiler.optimizationLevel=n'.
     * </p>
     * @parameter default-value="-1" expression="${gwt.compiler.optimizationLevel}"
     * @since 2.1.0-1
     */
    @Parameter(defaultValue = "-1", property = "gwt.compiler.optimizationLevel")
    private int optimizationLevel;

    /**
     * EXPERIMENTAL: Emit extra, detailed compile-report information in the "Story Of Your Compile" at the expense of compile time.
     * <p>
     * Can be set from command line using '-Dgwt.compiler.soycDetailed=true'.
     * </p>
     *
     * @since 2.1.0-1
     */
    @Parameter(alias = "soycDetailed", defaultValue = "false", property = "gwt.compiler.soycDetailed")
    private boolean detailedSoyc;

    /**
     * Fail compilation if any input file contains an error.
     * 
     * <p>
     * Can be set from command line using '-Dgwt.compiler.strict=true'.
     * </p>
     *
     * @since 2.1.0-1
     */
    @Parameter(alias = "strict", defaultValue = "false", property = "gwt.compiler.strict")
    private boolean failOnError;

    /**
     * EXPERIMENTAL: Compile output Javascript with the Closure compiler for even further optimizations.
     * <p>
     * Can be set from the command line using '-Dgwt.compiler.enableClosureCompiler=true'
     * </p>
     *
     * @since 2.5.0-rc1
     * @deprecated https://github.com/gwtproject/gwt/commit/162ccc9c9112a09bf9ea046da95760f5f1886b72
     */
    @Parameter(alias = "enableClosureCompiler", defaultValue = "false", property = "gwt.compiler.enableClosureCompiler")
    private boolean closureCompiler;

    /**
     * Enables Javascript output suitable for post-compilation by Closure Compiler.
     *
     * @since 2.8.0
     */
    @Parameter(defaultValue = "false", property = "gwt.compiler.closureFormattedOutput")
    private boolean closureFormattedOutput;

    /**
     * EXPERIMENTAL: Gather compiler metrics.
     * <p>
     * Can be set from the command line using '-Dgwt.compiler.compilerMetrics=true'
     * </p>
     *
     * @since 2.5.0-rc1
     */
    @Parameter(defaultValue = "false", property = "gwt.compiler.compilerMetrics")
    private boolean compilerMetrics;

    /**
     * EXPERIMENTAL: Limits of number of fragments using a code splitter that merges split points.
     * <p>
     * Can be set from the command line using '-Dgwt.compiler.fragmentCount=n'
     * </p>
     *
     * @since 2.5.0-rc1
     */
    @Parameter(defaultValue = "-1", property = "gwt.compiler.fragmentCount")
    private int fragmentCount;

    /**
     * EXPERIMENTAL: Cluster similar functions in the output to improve compression.
     *
     * @since 2.6.0-rc1
     */
    @Parameter(defaultValue = "true", property = "gwt.compiler.clusterFunctions")
    private boolean clusterFunctions;

    /**
     * EXPERIMENTAL: Inline literal parameters to shrink function declarations and
     * provide more deadcode elimination possibilities.
     *
     * @since 2.6.0-rc1
     */
    @Parameter(defaultValue = "true", property = "gwt.compiler.inlineLiteralParameters")
    private boolean inlineLiteralParameters;

    /**
     * EXPERIMENTAL: Analyze and optimize dataflow.
     *
     * since 2.6.0-rc1
     */
    @Parameter(defaultValue = "true", property = "gwt.compiler.optimizeDataflow")
    private boolean optimizeDataflow;

    /**
     * Generate exports for JsInterop purposes.
     *
     * @since 2.8.0-rc1
     */
    @Parameter(defaultValue = "false", property = "gwt.compiler.generateJsInteropExports")
    private boolean generateJsInteropExports;

    /**
     * EXPERIMENTAL: Ordinalize enums to reduce some large strings.
     *
     * @since 2.6.0-rc1
     */
    @Parameter(defaultValue = "true", property = "gwt.compiler.ordinalizeEnums")
    private boolean ordinalizeEnums;

    /**
     * EXPERIMENTAL: Removing duplicate functions.
     * <p>
     * Will interfere with stacktrace deobfuscation and so is only honored when compiler.stackMode is set to strip.
     *
     * @since 2.6.0-rc1
     */
    @Parameter(defaultValue = "true", property = "gwt.compiler.removeDuplicateFunctions")
    private boolean removeDuplicateFunctions;

    /**
     * Enables saving source code needed by debuggers.
     *
     * @since 2.6.0-rc1
     */
    @Parameter(defaultValue = "false", property = "gwt.saveSource")
    private boolean saveSource;

    /**
     * Overrides where source files useful to debuggers will be written.
     * <p>
     * Default: saved with extras.
     *
     * @since 2.6.0-rc2
     */
    @Parameter
    private File saveSourceOutput;

    /**
     * Specifies Java source level.
     *
     * @since 2.6.0-rc1
     */
    @Parameter(defaultValue = "auto", property = "maven.compiler.source")
    private String sourceLevel;

    /**
     * Puts most JavaScript globals into namespaces.
     * <p>
     * Value is one of PACKAGE or NONE.
     * <p>
     * Default: PACKAGE for -draftCompile, otherwise NONE
     * 
     * @since 2.7.0-rc1
     */
    @Parameter
    private String namespace;

    @Parameter(defaultValue = "false")
    private boolean overlappingSourceWarnings;

    /**
     * EXPERIMENTAL: Emit detailed compile-report information in the "Story Of Your Compile"  in the new json format.
     * 
     * @since 2.7.0-rc1
     */
    @Parameter(defaultValue = "false")
    private boolean enableJsonSoyc;

    /**
     * Compiles faster by reusing data from the previous compile.
     * 
     * @since 2.7.0-rc1
     */
    @Parameter(alias = "compilePerFile", defaultValue = "false", property = "gwt.compiler.incremental")
    private boolean incremental;

    /**
     * EXPERIMENTAL: Emit extra information allow chrome dev tools to display Java identifiers in many places instead of JavaScript functions.
     * <p>
     * Value can be one of NONE, ONLY_METHOD_NAME, ABBREVIATED or FULL.
     * 
     * @since 2.7.0-rc1
     */
    @Parameter(defaultValue = "NONE", property = "gwt.compiler.methodNameDisplayMode")
    private String methodNameDisplayMode;

    /**
     * Indicates whether to print the whole Java command used for GWT compilation, in case of an error. If set to false,
     * the command is not printed.
     *
     * Ability to disable the command comes handy in case the build uses big number (hundreds) of jars. In that case the
     * classpath string is huge and the printed command is polluting the Maven build log.
     */
    @Parameter(defaultValue = "true", property = "gwt.compiler.printJavaCommandOnError" )
    private boolean printJavaCommandOnError;

    /**
     * Code coverage generation configuration.
     */
    @Parameter
    private CoverageConfiguration coverage;

    /**
     * Additional arguments to be passed to the GWT compiler.
     *
     * @since 2.9.1 (since 2.7.0 of GWT compiler)
     */
    @Parameter
    private List<String> compilerArgs;

    public void doExecute( )
        throws MojoExecutionException, MojoFailureException
    {
        if ( skip || "pom".equals( getProject().getPackaging() ) )
        {
            getLog().info( "GWT compilation is skipped" );
            return;
        }

        if ( !this.getOutputDirectory().exists() )
        {
            this.getOutputDirectory().mkdirs();
        }

        compile( getModules() );
    }

    @Override
    protected String getExtraJvmArgs()
    {
        String jvmArgs = super.getExtraJvmArgs();
        // workaround to GWT issue 4031 with IBM JDK
        // @see https://code.google.com/p/google-web-toolkit/issues/detail?id=4031#c16
        if ( System.getProperty( "java.vendor" ).startsWith( "IBM" ) && StringUtils.isEmpty(getJvm()) && !StringUtils.isEmpty( jvmArgs ))
        {
            return jvmArgs + " -Dgwt.jjs.javaArgs=" + StringUtils.quoteAndEscape( jvmArgs, '"', new char[] { '"', ' ', '\t', '\r', '\n' } );
        }
        return jvmArgs;
    }

    private void compile( String[] modules )
        throws MojoExecutionException
    {
        boolean upToDate = true;

        JavaCommand cmd = createJavaCommand()
            .setMainClass( "com.google.gwt.dev.Compiler" );
        if ( gwtSdkFirstInClasspath )
        {
            cmd.addToClasspath( getGwtUserJar() )
               .addToClasspath( getGwtDevJar() );
        }
        cmd.addToClasspath( getClasspath( Artifact.SCOPE_COMPILE ) );
        if ( !gwtSdkFirstInClasspath )
        {
            cmd.addToClasspath( getGwtUserJar() )
               .addToClasspath( getGwtDevJar() );
        }

        cmd.arg( "-logLevel", getLogLevel() )
            .arg( "-war", getOutputDirectory().getAbsolutePath() )
            .arg( "-localWorkers", String.valueOf( getLocalWorkers() ) )
            // optional advanced arguments
            .arg( checkAssertions, "-checkAssertions" )
            .arg( draftCompile, "-draftCompile" )
            .arg( validateOnly, "-validateOnly" )
            .arg( disableClassMetadata, "-XnoclassMetadata" )
            .arg( disableCastChecking, "-XnocheckCasts" )
            .arg( disableRunAsync, "-XnocodeSplitting" )
            .arg( failOnError, "-failOnError" )
            .arg( detailedSoyc, "-XdetailedSoyc" )
            .arg( closureCompiler, "-XclosureCompiler" )
            .arg( closureFormattedOutput, "-XclosureFormattedOutput" )
            .arg( compileReport, "-compileReport" )
            .arg( compilerMetrics, "-XcompilerMetrics" )
            .arg( "-XfragmentCount", String.valueOf( fragmentCount ) )
            .arg( !clusterFunctions, "-XnoclusterFunctions" )
            .arg( !inlineLiteralParameters, "-XnoinlineLiteralParameters" )
            .arg( !optimizeDataflow, "-XnooptimizeDataflow" )
            .arg( !ordinalizeEnums, "-XnoordinalizeEnums" )
            .arg( !removeDuplicateFunctions, "-XnoremoveDuplicateFunctions" )
            .arg( saveSource, "-saveSource" )
            .arg( "-sourceLevel", sourceLevel )
            .arg( enableJsonSoyc, "-XenableJsonSoyc" )
            .arg( incremental, "-incremental" )
            .arg( generateJsInteropExports, "-generateJsInteropExports" )
        ;

        if ( style != null && style.length() > 0 )
        {
            cmd.arg( "-style", style );
        }

        if ( methodNameDisplayMode != null && methodNameDisplayMode.length() > 0 && !methodNameDisplayMode.equals( "NONE" ))
        {
            cmd.arg( "-XmethodNameDisplayMode", methodNameDisplayMode );
        }

        if ( namespace != null && namespace.length() > 0 )
        {
            cmd.arg( "-Xnamespace", namespace );
        }

        if ( saveSourceOutput != null )
        {
            cmd.arg( "-saveSourceOutput", saveSourceOutput.getAbsolutePath() );
        }

        if ( optimizationLevel >= 0 )
        {
            cmd.arg( "-optimize" ).arg( Integer.toString( optimizationLevel ) );
        }

        if ( extraParam || compileReport || ( saveSource && saveSourceOutput == null ) )
        {
            getLog().debug( "create extra directory " );
            if ( !extra.exists() )
            {
                extra.mkdirs();
            }
            cmd.arg( "-extra" ).arg( extra.getAbsolutePath() );
        }
        else
        {
            getLog().debug( "NOT create extra directory " );
        }

        addCoverageArgument( cmd );
        addCompileSourceArtifacts( cmd );
        addArgumentDeploy(cmd);
        addArgumentGen( cmd );
        addPersistentUnitCache(cmd);

        if ( workDir != null )
        {
            cmd.arg( "-workDir" ).arg( String.valueOf( workDir ) );
        }

        if (compilerArgs != null) {
            for (String compilerArg : compilerArgs)
            {
                cmd.arg(compilerArg);
            }
        }

        for ( String target : modules )
        {
            if ( !compilationRequired( target, getOutputDirectory() ) )
            {
                continue;
            }
            cmd.arg( target );
            upToDate = false;
        }

        cmd.setPrintCommandOnError(printJavaCommandOnError);

        if ( !upToDate )
        {
            try
            {
                cmd.execute();
            }
            catch ( JavaCommandException e )
            {
                throw new MojoExecutionException( e.getMessage(), e );
            }
        }
    }

    private int getLocalWorkers()
    {
        if ( localWorkers > 0 )
        {
            return localWorkers;
        }
        return Runtime.getRuntime().availableProcessors();
    }

    /**
     * Try to find out, if there are stale sources. If aren't some, we don't have to compile... ...this heuristic
     * doesn't take into account, that there could be updated dependencies. But for this case, as 'clean compile' could
     * be executed which would force a compilation.
     *
     * @param module Name of the GWT module to compile
     * @param output Output path
     * @return true if compilation is required (i.e. stale sources are found)
     * @throws MojoExecutionException When sources scanning fails
     * @author Alexander Gordt
     */
    private boolean compilationRequired( String module, File output )
        throws MojoExecutionException
    {
        getLog().debug( "**Checking if compilation is required for " + module );
        try
        {

        	GwtModule gwtModule = readModule( module );
            if ( gwtModule.getEntryPoints().size() == 0 )
            {
                getLog().info( gwtModule.getName() + " has no EntryPoint - compilation skipped" );
                // No entry-point, this is an utility module : compiling this one will fail
                // with '[ERROR] Module has no entry points defined'
                return false;
            }
            getLog().debug( "Module has an entrypoint" );

            if ( force )
            {
                return true;
            }
            getLog().debug( "Compilation not forced");
            
            String modulePath = gwtModule.getPath();

            String outputTarget = modulePath + "/" + modulePath + ".nocache.js";
            File outputTargetFile = new File( output, outputTarget );
            // Require compilation if no js file present in target.
            if ( !outputTargetFile.exists() )
            {
                return true;
            }
            getLog().debug( "Output file exists");
            
            File moduleFile = gwtModule.getSourceFile();
            if(moduleFile == null) {
            	return true; //the module was read from something like an InputStream; always recompile this because we can't make any other choice
            }
            getLog().debug( "There is a module source file (not an input stream");
            
            //If input is newer than target, recompile
            if(moduleFile.lastModified() > outputTargetFile.lastModified()) 
            {
                getLog().debug( "Module file has been modified since the output file was created; recompiling" );
            	return true;
            }
            getLog().debug( "The module XML hasn't been updated");

            // js file already exists, but may not be up-to-date with project source files
            SingleTargetSourceMapping singleTargetMapping = new SingleTargetSourceMapping( ".java", outputTarget );
            StaleSourceScanner scanner = new StaleSourceScanner();
            scanner.addSourceMapping( singleTargetMapping );

            SingleTargetSourceMapping uiBinderMapping = new SingleTargetSourceMapping( ".ui.xml", outputTarget );
            scanner.addSourceMapping( uiBinderMapping );

            Collection<File> compileSourceRoots = new HashSet<File>();
            for (String sourceRoot : getProject().getCompileSourceRoots()) {
                for (String sourcePackage : gwtModule.getSources()) {
                    String packagePath = gwtModule.getPackage().replace( '.', File.separatorChar );
                    File sourceDirectory = new File (sourceRoot + File.separatorChar + packagePath + File.separator + sourcePackage);
                    if(sourceDirectory.exists()) {
                        getLog().debug(" Looking in a source directory "+sourceDirectory.getAbsolutePath() + " for possible changes");
                        compileSourceRoots.add(sourceDirectory);					
                    }
                }
            }

            for ( File sourceRoot : compileSourceRoots )
            {
                if ( !sourceRoot.isDirectory() )
                {
                    continue;
                }
                try
                {
                    if ( !scanner.getIncludedSources( sourceRoot, output ).isEmpty() )
                    {
                        getLog().debug( "found stale source in " + sourceRoot + " compared with " + output );
                        return true;
                    }
                }
                catch ( InclusionScanException e )
                {
                    throw new MojoExecutionException( "Error scanning source root: \'" + sourceRoot + "\' "
                        + "for stale files to recompile.", e );
                }
            }
            getLog().info( module + " is up to date. GWT compilation skipped" );
            return false;
        }
        catch ( GwtModuleReaderException e )
        {
            throw new MojoExecutionException( e.getMessage(), e );
        }
    }

    private void addCoverageArgument( JavaCommand cmd )
        throws MojoExecutionException
    {
        if ( coverage != null && coverage.isEnabled() )
        {
            SortedSet<String> files = getFilesToCover();
            if ( files.isEmpty() )
            {
                getLog().warn( "Nothing to instrument" );
            }
            else
            {
                getLog().info( "Will instrument " + files.size() + " files for GWT code coverage" );
                try
                {
                    String buildDirectory = getProject().getBuild().getDirectory();
                    File outFile = new File( buildDirectory, "gwt-coverage.in" );
                    PrintStream stream = new PrintStream(outFile);
                    for ( String file : files )
                    {
                        stream.println( file );
                    }
                    cmd.systemProperty( "gwt.coverage", outFile.getAbsolutePath() );
                }
                catch ( IOException e )
                {
                    getLog().warn( "Could not prepare the files for instrumentation", e );
                }
            }
        }
    }

    private SortedSet<String> getFilesToCover()
        throws MojoExecutionException
    {
        getLog().info( "Building the list of source files to instrument" );
        getLog().info( "  Includes: " + Arrays.toString(coverage.getIncludes()) );
        getLog().info( "  Excludes: " + Arrays.toString( coverage.getExcludes() ) );
        getLog().info( "  Scan dependencies: " + ( coverage.isScanDependencies() ? "yes" : "no" ) );

        ProjectScanner scanner = new ProjectScanner( getLog() );
        scanner.setProject(getProject());
        if ( coverage.isScanDependencies() )
        {
            scanner.setArtifacts(getClasspath(Artifact.SCOPE_COMPILE));
        }

        scanner.setIncludes(coverage.getIncludes());
        scanner.setExcludes(coverage.getExcludes());
        scanner.scan();

        String[] includedFiles = scanner.getIncludedFiles();

        SortedSet<String> sortedFiles = new TreeSet<String>();
        Collections.addAll( sortedFiles, includedFiles );
        return sortedFiles;
    }
}
