/*
 *  Licensed to the Apache Software Foundation (ASF) under one
 *  or more contributor license agreements.  See the NOTICE file
 *  distributed with this work for additional information
 *  regarding copyright ownership.  The ASF licenses this file
 *  to you under the Apache License, Version 2.0 (the
 *  "License"); you may not use this file except in compliance
 *  with the License.  You may obtain a copy of the License at
 *
 *    http://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing,
 *  software distributed under the License is distributed on an
 *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 *  KIND, either express or implied.  See the License for the
 *  specific language governing permissions and limitations
 *  under the License.
 */
package org.codehaus.groovy.ant;

import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import groovy.lang.GroovyClassLoader;
import org.antlr.v4.runtime.tree.ParseTreeVisitor;
import org.apache.groovy.io.StringBuilderWriter;
import org.apache.tools.ant.AntClassLoader;
import org.apache.tools.ant.BuildException;
import org.apache.tools.ant.DirectoryScanner;
import org.apache.tools.ant.RuntimeConfigurable;
import org.apache.tools.ant.taskdefs.Execute;
import org.apache.tools.ant.taskdefs.Javac;
import org.apache.tools.ant.taskdefs.MatchingTask;
import org.apache.tools.ant.types.Path;
import org.apache.tools.ant.types.Reference;
import org.apache.tools.ant.util.GlobPatternMapper;
import org.apache.tools.ant.util.SourceFileScanner;
import org.codehaus.groovy.GroovyBugError;
import org.codehaus.groovy.control.CompilationUnit;
import org.codehaus.groovy.control.CompilerConfiguration;
import org.codehaus.groovy.control.SourceExtensionHandler;
import org.codehaus.groovy.runtime.DefaultGroovyMethods;
import org.codehaus.groovy.runtime.DefaultGroovyStaticMethods;
import org.codehaus.groovy.tools.ErrorReporter;
import org.codehaus.groovy.tools.FileSystemCompiler;
import org.codehaus.groovy.tools.javac.JavaAwareCompilationUnit;
import org.objectweb.asm.ClassVisitor;
import picocli.CommandLine;

import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.io.PrintWriter;
import java.io.Writer;
import java.net.URI;
import java.net.URISyntaxException;
import java.nio.charset.Charset;
import java.security.PrivilegedAction;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.StringTokenizer;

/**
 * Compiles Groovy source files using Ant.
 * <p>
 * Typically involves using Ant from the command-line and an Ant build file such as:
 * <pre>
 * &lt;?xml version="1.0"?&gt;
 * &lt;project name="MyGroovyBuild" default="compile"&gt;
 *   &lt;property name="groovy.home" location="/Path/To/Groovy"/&gt;
 *   &lt;property name="groovy.version" value="X.Y.Z"/&gt;
 *
 *   &lt;taskdef name="groovyc" classname="org.codehaus.groovy.ant.Groovyc"&gt;
 *     &lt;classpath&gt;
 *       &lt;fileset file="${groovy.home}/lib/groovy-${groovy.version}.jar"/&gt;
 *       &lt;fileset file="${groovy.home}/lib/groovy-ant-${groovy.version}.jar"/&gt;
 *     &lt;/classpath&gt;
 *   &lt;/taskdef&gt;
 *
 *   &lt;target name="compile" description="compile groovy sources"&gt;
 *     &lt;groovyc srcdir="src" destdir="bin" fork="true" listfiles="true" includeantruntime="false"&gt;
 *       &lt;classpath&gt;
 *         &lt;fileset dir="${groovy.home}/lib" includes="groovy-*${groovy.version}.jar" excludes="groovy-ant-${groovy.version}.jar"/&gt;
 *       &lt;/classpath&gt;
 *     &lt;/groovyc&gt;
 *   &lt;/target&gt;
 * &lt;/project&gt;
 * </pre>
 * <p>
 * This task can take the following arguments:
 * <ul>
 * <li>srcdir</li>
 * <li>destdir</li>
 * <li>sourcepath</li>
 * <li>sourcepathRef</li>
 * <li>classpath</li>
 * <li>classpathRef</li>
 * <li>scriptExtension</li>
 * <li>targetBytecode</li>
 * <li>listfiles</li>
 * <li>failonerror</li>
 * <li>proceed</li>
 * <li>memoryInitialSize</li>
 * <li>memoryMaximumSize</li>
 * <li>encoding</li>
 * <li>verbose</li>
 * <li>includeantruntime</li>
 * <li>includejavaruntime</li>
 * <li>fork</li>
 * <li>javaHome</li>
 * <li>executable</li>
 * <li>updatedProperty</li>
 * <li>errorProperty</li>
 * <li>includeDestClasses</li>
 * <li>jointCompilationOptions</li>
 * <li>stacktrace</li>
 * <li>scriptBaseClass</li>
 * <li>stubdir</li>
 * <li>keepStubs</li>
 * <li>forceLookupUnnamedFiles</li>
 * <li>configscript</li>
 * <li>parameters</li>
 * </ul>
 * And these nested tasks:
 * <ul>
 * <li>javac</li>
 * </ul>
 * Of these arguments, the <b>srcdir</b> and <b>destdir</b> are required.
 * <p>
 * When this task executes, it will recursively scan srcdir and destdir looking
 * for Groovy source files to compile. This task makes its compile decision based
 * on timestamp.
 * <p>
 * A more elaborate build file showing joint compilation:
 * <pre>
 * &lt;?xml version="1.0"?&gt;
 * &lt;project name="MyJointBuild" default="compile"&gt;
 *   &lt;property name="groovy.home" location="/Path/To/Groovy"/&gt;
 *   &lt;property name="groovy.version" value="X.Y.Z"/&gt;
 *
 *   &lt;path id="classpath.main"&gt;
 *     &lt;fileset dir="${groovy.home}/lib"&gt;
 *       &lt;include name="groovy-*${groovy.version}.jar"/&gt;
 *       &lt;exclude name="groovy-ant-${groovy.version}.jar"/&gt;
 *     &lt;/fileset&gt;
 *   &lt;/path&gt;
 *
 *   &lt;taskdef name="groovyc" classname="org.codehaus.groovy.ant.Groovyc"&gt;
 *     &lt;classpath&gt;
 *       &lt;fileset file="${groovy.home}/lib/groovy-${groovy.version}.jar"/&gt;
 *       &lt;fileset file="${groovy.home}/lib/groovy-ant-${groovy.version}.jar"/&gt;
 *     &lt;/classpath&gt;
 *   &lt;/taskdef&gt;
 *
 *   &lt;target name="clean"&gt;
 *     &lt;delete dir="bin" failonerror="false"/&gt;
 *   &lt;/target&gt;
 *
 *   &lt;target name="compile" depends="clean" description="compile java and groovy sources"&gt;
 *     &lt;mkdir dir="bin"/&gt;
 *
 *     &lt;groovyc srcdir="src" destdir="bin" stubdir="stubs" keepStubs="true"
 *      fork="true" includeantruntime="false" classpathref="classpath.main"&gt;
 *       &lt;javac debug="true" source="1.8" target="1.8"/&gt;
 *     &lt;/groovyc&gt;
 *   &lt;/target&gt;
 * &lt;/project&gt;
 * </pre>
 * <p>
 * Based on the implementation of the Javac task in Apache Ant.
 * <p>
 * Can also be used from {@link groovy.ant.AntBuilder} to allow the build file to be scripted in Groovy.
 */
public class Groovyc extends MatchingTask {

    private static final File[] EMPTY_FILE_ARRAY = new File[0];
    private static final String[] EMPTY_STRING_ARRAY = new String[0];

    private final LoggingHelper log = new LoggingHelper(this);

    private Path src;
    private File destDir;
    private Path compileClasspath;
    private Path compileSourcepath;
    private String encoding;
    private boolean stacktrace;
    private boolean verbose;
    private boolean includeAntRuntime = true;
    private boolean includeJavaRuntime;
    private boolean fork;
    private File forkJavaHome;
    private String forkedExecutable;
    private String memoryInitialSize;
    private String memoryMaximumSize;
    private String scriptExtension = "*.groovy";
    private String targetBytecode;

    protected boolean failOnError = true;
    protected boolean listFiles;
    protected File[] compileList = EMPTY_FILE_ARRAY;

    private String updatedProperty;
    private String errorProperty;
    private boolean taskSuccess = true;
    private boolean includeDestClasses = true;

    protected CompilerConfiguration configuration;
    private Javac javac;
    private boolean jointCompilation;

    private final List<File> temporaryFiles = new ArrayList<>(2);
    private File stubDir;
    private boolean keepStubs;
    private boolean forceLookupUnnamedFiles;
    private String scriptBaseClass;
    private String configscript;

    private Set<String> scriptExtensions = new LinkedHashSet<>();

    /**
     * If true, generates metadata for reflection on method parameter names (jdk8+ only).  Defaults to false.
     */
    private boolean parameters;

    /**
     * If true, enable preview Java features (JEP 12) (jdk12+ only). Defaults to false.
     */
    private boolean previewFeatures;

    /**
     * Adds a path for source compilation.
     *
     * @return a nested src element.
     */
    public Path createSrc() {
        if (src == null) {
            src = new Path(getProject());
        }
        return src.createPath();
    }

    /**
     * Recreate src.
     *
     * @return a nested src element.
     */
    protected Path recreateSrc() {
        src = null;
        return createSrc();
    }

    /**
     * Set the source directories to find the source Java files.
     *
     * @param srcDir the source directories as a path
     */
    public void setSrcdir(Path srcDir) {
        if (src == null) {
            src = srcDir;
        } else {
            src.append(srcDir);
        }
    }

    /**
     * Gets the source dirs to find the source java files.
     *
     * @return the source directories as a path
     */
    public Path getSrcdir() {
        return src;
    }

    /**
     * Set the extension to use when searching for Groovy source files.
     * Accepts extensions in the form *.groovy, .groovy or groovy
     *
     * @param scriptExtension the extension of Groovy source files
     */
    public void setScriptExtension(String scriptExtension) {
        if (scriptExtension.startsWith("*.")) {
            this.scriptExtension = scriptExtension;
        } else if (scriptExtension.startsWith(".")) {
            this.scriptExtension = "*" + scriptExtension;
        } else {
            this.scriptExtension = "*." + scriptExtension;
        }
    }

    /**
     * Get the extension to use when searching for Groovy source files.
     *
     * @return the extension of Groovy source files
     */
    public String getScriptExtension() {
        return scriptExtension;
    }

    /**
     * Sets the bytecode compatibility level.
     * The parameter can take one of the values in {@link CompilerConfiguration#ALLOWED_JDKS}.
     *
     * @param version the bytecode compatibility level
     */
    public void setTargetBytecode(String version) {
        for (String allowedJdk : CompilerConfiguration.ALLOWED_JDKS) {
            if (allowedJdk.equals(version)) {
                this.targetBytecode = version;
                break;
            }
        }
    }

    /**
     * Retrieves the compiler bytecode compatibility level.
     *
     * @return bytecode compatibility level. Can be one of the values in {@link CompilerConfiguration#ALLOWED_JDKS}.
     */
    public String getTargetBytecode() {
        return this.targetBytecode;
    }

    /**
     * Set the destination directory into which the Java source
     * files should be compiled.
     *
     * @param destDir the destination director
     */
    public void setDestdir(File destDir) {
        this.destDir = destDir;
    }

    /**
     * Gets the destination directory into which the java source files
     * should be compiled.
     *
     * @return the destination directory
     */
    public File getDestdir() {
        return destDir;
    }

    /**
     * Set the sourcepath to be used for this compilation.
     *
     * @param sourcepath the source path
     */
    public void setSourcepath(Path sourcepath) {
        if (compileSourcepath == null) {
            compileSourcepath = sourcepath;
        } else {
            compileSourcepath.append(sourcepath);
        }
    }

    /**
     * Gets the sourcepath to be used for this compilation.
     *
     * @return the source path
     */
    public Path getSourcepath() {
        return compileSourcepath;
    }

    /**
     * Adds a path to sourcepath.
     *
     * @return a sourcepath to be configured
     */
    public Path createSourcepath() {
        if (compileSourcepath == null) {
            compileSourcepath = new Path(getProject());
        }
        return compileSourcepath.createPath();
    }

    /**
     * Adds a reference to a source path defined elsewhere.
     *
     * @param r a reference to a source path
     */
    public void setSourcepathRef(Reference r) {
        createSourcepath().setRefid(r);
    }

    /**
     * Set the classpath to be used for this compilation.
     *
     * @param classpath an Ant Path object containing the compilation classpath.
     */
    public void setClasspath(Path classpath) {
        if (compileClasspath == null) {
            compileClasspath = classpath;
        } else {
            compileClasspath.append(classpath);
        }
    }

    /**
     * Gets the classpath to be used for this compilation.
     *
     * @return the class path
     */
    public Path getClasspath() {
        return compileClasspath;
    }

    /**
     * Adds a path to the classpath.
     *
     * @return a class path to be configured
     */
    public Path createClasspath() {
        if (compileClasspath == null) {
            compileClasspath = new Path(getProject());
        }
        return compileClasspath.createPath();
    }

    /**
     * Adds a reference to a classpath defined elsewhere.
     *
     * @param r a reference to a classpath
     */
    public void setClasspathRef(Reference r) {
        createClasspath().setRefid(r);
    }

    /**
     * If true, list the source files being handed off to the compiler.
     * Default is false.
     *
     * @param list if true list the source files
     */
    public void setListfiles(boolean list) {
        listFiles = list;
    }

    /**
     * Get the listfiles flag.
     *
     * @return the listfiles flag
     */
    public boolean getListfiles() {
        return listFiles;
    }

    /**
     * Indicates whether the build will continue
     * even if there are compilation errors; defaults to true.
     *
     * @param fail if true halt the build on failure
     */
    public void setFailonerror(boolean fail) {
        failOnError = fail;
    }

    /**
     * @param proceed inverse of failonerror
     */
    public void setProceed(boolean proceed) {
        failOnError = !proceed;
    }

    /**
     * Gets the failonerror flag.
     *
     * @return the failonerror flag
     */
    public boolean getFailonerror() {
        return failOnError;
    }

    /**
     * The initial size of the memory for the underlying VM
     * if javac is run externally; ignored otherwise.
     * Defaults to the standard VM memory setting.
     * (Examples: 83886080, 81920k, or 80m)
     *
     * @param memoryInitialSize string to pass to VM
     */
    public void setMemoryInitialSize(String memoryInitialSize) {
        this.memoryInitialSize = memoryInitialSize;
    }

    /**
     * Gets the memoryInitialSize flag.
     *
     * @return the memoryInitialSize flag
     */
    public String getMemoryInitialSize() {
        return memoryInitialSize;
    }

    /**
     * The maximum size of the memory for the underlying VM
     * if javac is run externally; ignored otherwise.
     * Defaults to the standard VM memory setting.
     * (Examples: 83886080, 81920k, or 80m)
     *
     * @param memoryMaximumSize string to pass to VM
     */
    public void setMemoryMaximumSize(String memoryMaximumSize) {
        this.memoryMaximumSize = memoryMaximumSize;
    }

    /**
     * Gets the memoryMaximumSize flag.
     *
     * @return the memoryMaximumSize flag
     */
    public String getMemoryMaximumSize() {
        return memoryMaximumSize;
    }

    /**
     * Sets the file encoding for generated files.
     *
     * @param encoding the file encoding to be used
     */
    public void setEncoding(String encoding) {
        this.encoding = encoding;
    }

    /**
     * Returns the encoding to be used when creating files.
     *
     * @return the file encoding to use
     */
    public String getEncoding() {
        return encoding;
    }

    /**
     * Enable verbose compiling which will display which files
     * are being compiled. Default is false.
     */
    public void setVerbose(boolean verbose) {
        this.verbose = verbose;
    }

    /**
     * Gets the verbose flag.
     *
     * @return the verbose flag
     */
    public boolean getVerbose() {
        return verbose;
    }

    /**
     * If true, includes Ant's own classpath in the classpath. Default is true.
     * If setting to false and using groovyc in conjunction with AntBuilder
     * you might need to explicitly add the Groovy jar(s) to the groovyc
     * classpath using a nested classpath task.
     *
     * @param include if true, includes Ant's own classpath in the classpath
     */
    public void setIncludeantruntime(boolean include) {
        includeAntRuntime = include;
    }

    /**
     * Gets whether the ant classpath is to be included in the classpath.
     *
     * @return whether the ant classpath is to be included in the classpath
     */
    public boolean getIncludeantruntime() {
        return includeAntRuntime;
    }

    /**
     * If true, includes the Java runtime libraries in the classpath. Default is false.
     *
     * @param include if true, includes the Java runtime libraries in the classpath
     */
    public void setIncludejavaruntime(boolean include) {
        includeJavaRuntime = include;
    }

    /**
     * Gets whether the java runtime should be included in this
     * task's classpath.
     *
     * @return the includejavaruntime attribute
     */
    public boolean getIncludejavaruntime() {
        return includeJavaRuntime;
    }

    /**
     * If true forks the Groovy compiler. Default is false.
     *
     * @param f "true|false|on|off|yes|no"
     */
    public void setFork(boolean f) {
        fork = f;
    }

    /**
     * The JDK Home to use when forked.
     * Ignored if "executable" is specified.
     *
     * @param home the java.home value to use, default is the current JDK's home
     */
    public void setJavaHome(File home) {
        forkJavaHome = home;
    }

    /**
     * Sets the name of the java executable to use when
     * invoking the compiler in forked mode, ignored otherwise.
     *
     * @param forkExecPath the name of the executable
     * @since Groovy 1.8.7
     */
    public void setExecutable(String forkExecPath) {
        forkedExecutable = forkExecPath;
    }

    /**
     * The value of the executable attribute, if any.
     *
     * @return the name of the java executable
     * @since Groovy 1.8.7
     */
    public String getExecutable() {
        return forkedExecutable;
    }

    /**
     * The property to set on compilation success.
     * This property will not be set if the compilation
     * fails, or if there are no files to compile.
     *
     * @param updatedProperty the property name to use.
     */
    public void setUpdatedProperty(String updatedProperty) {
        this.updatedProperty = updatedProperty;
    }

    /**
     * The property to set on compilation failure.
     * This property will be set if the compilation
     * fails.
     *
     * @param errorProperty the property name to use.
     */
    public void setErrorProperty(String errorProperty) {
        this.errorProperty = errorProperty;
    }

    /**
     * This property controls whether to include the
     * destination classes directory in the classpath
     * given to the compiler.
     * The default value is "true".
     *
     * @param includeDestClasses the value to use.
     */
    public void setIncludeDestClasses(boolean includeDestClasses) {
        this.includeDestClasses = includeDestClasses;
    }

    /**
     * Get the value of the includeDestClasses property.
     *
     * @return the value.
     */
    public boolean isIncludeDestClasses() {
        return includeDestClasses;
    }

    /**
     * Get the result of the groovyc task (success or failure).
     *
     * @return true if compilation succeeded, or
     * was not necessary, false if the compilation failed.
     */
    public boolean getTaskSuccess() {
        return taskSuccess;
    }

    /**
     * Add the configured nested javac task if present to initiate joint compilation.
     */
    public void addConfiguredJavac(final Javac javac) {
        this.javac = javac;
        jointCompilation = true;
    }

    /**
     * Enable compiler to report stack trace information if a problem occurs
     * during compilation. Default is false.
     */
    public void setStacktrace(boolean stacktrace) {
        this.stacktrace = stacktrace;
    }

    /**
     * Legacy method to set the indy flag (only true is allowed)
     *
     * @param indy true means invokedynamic support is active
     */
    @Deprecated
    public void setIndy(final boolean indy) {
        if (!indy) {
            throw new BuildException("Disabling indy is no longer supported!", getLocation());
        }
    }

    /**
     * Get the value of the indy flag (always true).
     */
    @Deprecated
    public boolean getIndy() {
        return true;
    }

    /**
     * Set the base script class name for the scripts (must derive from Script)
     *
     * @param scriptBaseClass Base class name for scripts (must derive from Script)
     */
    public void setScriptBaseClass(String scriptBaseClass) {
        this.scriptBaseClass = scriptBaseClass;
    }

    /**
     * Get the base script class name for the scripts (must derive from Script)
     *
     * @return Base class name for scripts (must derive from Script)
     */
    public String getScriptBaseClass() {
        return this.scriptBaseClass;
    }

    /**
     * Get the configuration file used to customize the compilation configuration.
     *
     * @return a path to a configuration script
     */
    public String getConfigscript() {
        return configscript;
    }

    /**
     * Set the configuration file used to customize the compilation configuration.
     *
     * @param configscript a path to a configuration script
     */
    public void setConfigscript(final String configscript) {
        this.configscript = configscript;
    }

    /**
     * Set the stub directory into which the Java source stub
     * files should be generated. The directory need not exist
     * and will not be deleted automatically - though its contents
     * will be cleared unless 'keepStubs' is true. Ignored when forked.
     *
     * @param stubDir the stub directory
     */
    public void setStubdir(File stubDir) {
        jointCompilation = true;
        this.stubDir = stubDir;
    }

    /**
     * Gets the stub directory into which the Java source stub
     * files should be generated
     *
     * @return the stub directory
     */
    public File getStubdir() {
        return stubDir;
    }

    /**
     * Set the keepStubs flag. Defaults to false. Set to true for debugging.
     * Ignored when forked.
     *
     * @param keepStubs should stubs be retained
     */
    public void setKeepStubs(boolean keepStubs) {
        this.keepStubs = keepStubs;
    }

    /**
     * Gets the keepStubs flag.
     *
     * @return the keepStubs flag
     */
    public boolean getKeepStubs() {
        return keepStubs;
    }

    /**
     * Set the forceLookupUnnamedFiles flag. Defaults to false.
     * <p>
     * The Groovyc Ant task is frequently used in the context of a build system
     * that knows the complete list of source files to be compiled. In such a
     * context, it is wasteful for the Groovy compiler to go searching the
     * classpath when looking for source files and hence by default the
     * Groovyc Ant task calls the compiler in a special mode with such searching
     * turned off. If you wish the compiler to search for source files then
     * you need to set this flag to {@code true}.
     *
     * @param forceLookupUnnamedFiles should unnamed source files be searched for on the classpath
     */
    public void setForceLookupUnnamedFiles(boolean forceLookupUnnamedFiles) {
        this.forceLookupUnnamedFiles = forceLookupUnnamedFiles;
    }

    /**
     * Gets the forceLookupUnnamedFiles flag.
     *
     * @return the forceLookupUnnamedFiles flag
     */
    public boolean getForceLookupUnnamedFiles() {
        return forceLookupUnnamedFiles;
    }

    /**
     * If true, generates metadata for reflection on method parameter names (jdk8+ only).  Defaults to false.
     *
     * @param parameters set to true to generate metadata.
     */
    public void setParameters(boolean parameters) {
        this.parameters = parameters;
    }

    /**
     * Returns true if parameter metadata generation has been enabled.
     */
    public boolean getParameters() {
        return this.parameters;
    }

    /**
     * If true, enable preview Java features (JEP 12) (jdk12+ only).
     *
     * @param previewFeatures set to true to enable preview features
     */
    public void setPreviewFeatures(boolean previewFeatures) {
        this.previewFeatures = previewFeatures;
    }

    /**
     * Returns true if preview features has been enabled.
     */
    public boolean getPreviewFeatures() {
        return previewFeatures;
    }

    /**
     * Executes the task.
     *
     * @throws BuildException if an error occurs
     */
    @Override
    public void execute() throws BuildException {
        checkParameters();
        resetFileLists();
        loadRegisteredScriptExtensions();

        if (javac != null) jointCompilation = true;

        // scan source directories and dest directory to build up
        // compile lists
        String[] list = src.list();
        for (String filename : list) {
            File file = getProject().resolveFile(filename);
            if (!file.exists()) {
                throw new BuildException("srcdir \"" + file.getPath() + "\" does not exist!", getLocation());
            }
            DirectoryScanner ds = this.getDirectoryScanner(file);
            String[] files = ds.getIncludedFiles();
            scanDir(file, destDir != null ? destDir : file, files);
        }

        compile();
        if (updatedProperty != null
                && taskSuccess
                && compileList.length != 0) {
            getProject().setNewProperty(updatedProperty, "true");
        }
    }

    /**
     * Clear the list of files to be compiled and copied.
     */
    protected void resetFileLists() {
        compileList = EMPTY_FILE_ARRAY;
        scriptExtensions = new LinkedHashSet<>();
    }

    /**
     * Scans the directory looking for source files to be compiled.
     * The results are returned in the class variable compileList
     *
     * @param srcDir  The source directory
     * @param destDir The destination directory
     * @param files   An array of filenames
     */
    protected void scanDir(File srcDir, File destDir, String[] files) {
        GlobPatternMapper m = new GlobPatternMapper();
        SourceFileScanner sfs = new SourceFileScanner(this);
        File[] newFiles;
        for (String extension : getScriptExtensions()) {
            m.setFrom("*." + extension);
            m.setTo("*.class");
            newFiles = sfs.restrictAsFiles(files, srcDir, destDir, m);
            addToCompileList(newFiles);
        }

        if (jointCompilation) {
            m.setFrom("*.java");
            m.setTo("*.class");
            newFiles = sfs.restrictAsFiles(files, srcDir, destDir, m);
            addToCompileList(newFiles);
        }
    }

    protected void addToCompileList(File[] newFiles) {
        if (newFiles.length > 0) {
            File[] newCompileList = new File[compileList.length + newFiles.length];
            System.arraycopy(compileList, 0, newCompileList, 0, compileList.length);
            System.arraycopy(newFiles, 0, newCompileList, compileList.length, newFiles.length);
            compileList = newCompileList;
        }
    }

    /**
     * Gets the list of files to be compiled.
     *
     * @return the list of files as an array
     */
    public File[] getFileList() {
        return Arrays.copyOf(compileList, compileList.length);
    }

    protected void checkParameters() throws BuildException {
        if (src == null) {
            throw new BuildException("srcdir attribute must be set!", getLocation());
        }
        if (src.size() == 0) {
            throw new BuildException("srcdir attribute must be set!", getLocation());
        }

        if (destDir != null && !destDir.isDirectory()) {
            throw new BuildException("destination directory \""
                    + destDir
                    + "\" does not exist or is not a directory",
                    getLocation());
        }

        if (encoding != null && !Charset.isSupported(encoding)) {
            throw new BuildException("encoding \"" + encoding + "\" not supported.");
        }
    }

    private void listFiles() {
        if (listFiles) {
            for (File srcFile : compileList) {
                log.info(srcFile.getAbsolutePath());
            }
        }
    }

    /**
     * If {@code groovyc} task includes a nested {@code javac} task, check for
     * shareable configuration.  {@code FileSystemCompiler} supports several
     * command-line arguments for configuring joint compilation:
     * <ul>
     * <li><tt>-j</tt> enables joint compile
     * <li><tt>-F</tt> is used to pass flags
     * <li><tt>-J</tt> is used to pass name=value pairs
     * </ul>
     * Joint compilation options are transferred from {@link FileSystemCompiler}
     * to {@link CompilerConfiguration}'s jointCompileOptions property.  Flags
     * are saved to key "flags" (with the inclusion of "parameters" if enabled
     * on groovyc), pairs are saved to key "namedValues" and the key "memStub"
     * may also be set to {@link Boolean#TRUE} to influence joint compilation.
     *
     * @see org.codehaus.groovy.tools.javac.JavacJavaCompiler
     * @see javax.tools.JavaCompiler
     */
    private List<String> extractJointOptions(Path classpath) {
        List<String> jointOptions = new ArrayList<>();
        if (!jointCompilation) return jointOptions;

        // map "debug" and "debuglevel" to "-Fg"
        if (javac.getDebug()) {
            jointOptions.add("-Fg" + Optional.ofNullable(javac.getDebugLevel()).map(level -> ":" + level).orElse(""));
        } else {
            jointOptions.add("-Fg:none");
        }

        // map "deprecation" to "-Fdeprecation"
        if (javac.getDeprecation()) {
            jointOptions.add("-Fdeprecation");
        }

        // map "nowarn" to "-Fnowarn"
        if (javac.getNowarn()) {
            jointOptions.add("-Fnowarn");
        }

        // map "verbose" to "-Fverbose"
        if (javac.getVerbose()) {
            jointOptions.add("-Fverbose");
        }

        RuntimeConfigurable rc = javac.getRuntimeConfigurableWrapper();

        for (Map.Entry<String, Object> e : rc.getAttributeMap().entrySet()) {
            String key = e.getKey();
            if (key.equals("depend")
                    || key.equals("encoding")
                    || key.equals("extdirs")
                    || key.equals("nativeheaderdir")
                    || key.equals("release")
                    || key.equals("source")
                    || key.equals("target")) {
                switch (key) {
                    case "nativeheaderdir":
                        key = "h";
                        break;
                    case "release":
                        key = "-" + key; // to get "--" when passed to javac
                        break;
                    default:
                }
                // map "depend", "encoding", etc. to "-Jkey=val"
                jointOptions.add("-J" + key + "=" + getProject().replaceProperties(e.getValue().toString()));

            } else if (key.contains("classpath")) {
                if (key.startsWith("boot")) {
                    // map "bootclasspath" or "bootclasspathref" to "-Jbootclasspath="
                    jointOptions.add("-Jbootclasspath=" + javac.getBootclasspath());
                } else {
                    // map "classpath" or "classpathref" to "--classpath"
                    classpath.add(javac.getClasspath());
                }
            } else if (key.contains("module") && key.contains("path")) {
                if (key.startsWith("upgrade")) {
                    // map "upgrademodulepath" or "upgrademodulepathref" to "-J-upgrade-module-path="
                    jointOptions.add("-J-upgrade-module-path=" + javac.getUpgrademodulepath());
                } else if (key.contains("source")) {
                    // map "modulesourcepath" or "modulesourcepathref" to "-J-module-source-path="
                    jointOptions.add("-J-module-source-path=" + javac.getModulesourcepath());
                } else {
                    // map "modulepath" or "modulepathref" to "-J-module-path="
                    jointOptions.add("-J-module-path=" + javac.getModulepath());
                }
            } else if (!key.contains("debug") && !key.equals("deprecation") && !key.equals("nowarn") && !key.equals("verbose")) {
                log.warn("The option " + key + " cannot be set on the contained <javac> element. The option will be ignored.");
            }
            // TODO: defaultexcludes, excludes(file)?, includes(file)?, includeDestClasses, tempdir
        }

        // Ant's <javac> supports nested <compilerarg value=""> elements (there
        // can be multiple of them) for additional options to be passed to javac.
        for (RuntimeConfigurable childrc : Collections.list(rc.getChildren())) {
            if (childrc.getElementTag().equals("compilerarg")) {
                for (Map.Entry<String, Object> e : childrc.getAttributeMap().entrySet()) {
                    String key = e.getKey();
                    if (key.equals("value")) {
                        String value = getProject().replaceProperties(e.getValue().toString());
                        StringTokenizer st = new StringTokenizer(value, " ");
                        while (st.hasMoreTokens()) {
                            String option = st.nextToken();
                            // GROOVY-5063: map "-Werror", etc. to "-FWerror"
                            jointOptions.add(option.replaceFirst("^-(W|X|proc:)", "-F$1"));
                        }
                    }
                }
            }
        }

        return jointOptions;
    }

    private void doForkCommandLineList(List<String> commandLineList, Path classpath, String separator) {
        if (forkedExecutable != null && !forkedExecutable.isEmpty()) {
            commandLineList.add(forkedExecutable);
        } else {
            String javaHome;
            if (forkJavaHome != null) {
                javaHome = forkJavaHome.getPath();
            } else {
                javaHome = System.getProperty("java.home");
            }
            commandLineList.add(javaHome + separator + "bin" + separator + "java");
        }

        String[] bootstrapClasspath;
        ClassLoader loader = getClass().getClassLoader();
        if (loader instanceof AntClassLoader) {
            bootstrapClasspath = ((AntClassLoader) loader).getClasspath().split(File.pathSeparator);
        } else {
            Class<?>[] bootstrapClasses = {
                    FileSystemCompilerFacade.class,
                    FileSystemCompiler.class,
                    ParseTreeVisitor.class,
                    ClassVisitor.class,
                    CommandLine.class,
            };
            bootstrapClasspath = Arrays.stream(bootstrapClasses).map(Groovyc::getLocation)
                    .map(uri -> new File(uri).getAbsolutePath()).distinct().toArray(String[]::new);
        }
        if (bootstrapClasspath.length > 0) {
            commandLineList.add("-classpath");
            commandLineList.add(getClasspathRelative(bootstrapClasspath));
        }

        if (memoryInitialSize != null && !memoryInitialSize.isEmpty()) {
            commandLineList.add("-Xms" + memoryInitialSize);
        }
        if (memoryMaximumSize != null && !memoryMaximumSize.isEmpty()) {
            commandLineList.add("-Xmx" + memoryMaximumSize);
        }
        if (targetBytecode != null) {
            commandLineList.add("-Dgroovy.target.bytecode=" + targetBytecode);
        }
        if (!"*.groovy".equals(getScriptExtension())) {
            String tmpExtension = getScriptExtension();
            if (tmpExtension.startsWith("*."))
                tmpExtension = tmpExtension.substring(1);
            commandLineList.add("-Dgroovy.default.scriptExtension=" + tmpExtension);
        }

        commandLineList.add(FileSystemCompilerFacade.class.getName());
        commandLineList.add("--classpath");
        if (includeAntRuntime) {
            classpath.addExisting(new Path(getProject()).concatSystemClasspath("last"));
        }
        if (includeJavaRuntime) {
            classpath.addJavaRuntime();
        }
        commandLineList.add(getClasspathRelative(classpath.list()));
        if (forceLookupUnnamedFiles) {
            commandLineList.add("--forceLookupUnnamedFiles");
        }
    }

    private String getClasspathRelative(String[] classpath) {
        String baseDir = getProject().getBaseDir().getAbsolutePath();
        StringBuilder sb = new StringBuilder();
        for (String next : classpath) {
            if (sb.length() > 0) {
                sb.append(File.pathSeparatorChar);
            }
            if (next.startsWith(baseDir)) {
                sb.append(".").append(next, baseDir.length(), next.length());
            } else {
                sb.append(next);
            }
        }
        return sb.toString();
    }

    private static URI getLocation(Class<?> clazz) {
        try {
            return clazz.getProtectionDomain().getCodeSource().getLocation().toURI();
        } catch (URISyntaxException e) {
            throw new RuntimeException(e);
        }
    }

    /**
     * Add "groovyc" parameters to the commandLineList, based on the ant configuration.
     *
     * @param commandLineList
     * @param jointOptions
     * @param classpath
     */
    private void doNormalCommandLineList(List<String> commandLineList, List<String> jointOptions, Path classpath) {
        if (!fork) {
            commandLineList.add("--classpath");
            commandLineList.add(classpath.toString());
        }
        if (jointCompilation) {
            commandLineList.add("-j");
            commandLineList.addAll(jointOptions);
        }
        if (destDir != null) {
            commandLineList.add("-d");
            commandLineList.add(destDir.getPath());
        }
        if (encoding != null) {
            commandLineList.add("--encoding");
            commandLineList.add(encoding);
        }
        if (stacktrace) {
            commandLineList.add("-e");
        }
        if (parameters) {
            commandLineList.add("--parameters");
        }
        if (previewFeatures) {
            commandLineList.add("--enable-preview");
        }
        if (scriptBaseClass != null) {
            commandLineList.add("-b");
            commandLineList.add(scriptBaseClass);
        }
        if (configscript != null) {
            commandLineList.add("--configscript");
            commandLineList.add(configscript);
        }
    }

    private void addSourceFiles(List<String> commandLineList) {
        // check to see if an external file is needed
        int count = 0;
        if (fork) {
            for (File srcFile : compileList) {
                count += srcFile.getPath().length();
            }
            for (Object commandLineArg : commandLineList) {
                count += commandLineArg.toString().length();
            }
            count += compileList.length;
            count += commandLineList.size();
        }
        // 32767 is the command line length limit on Windows
        if (fork && (count > 32767)) {
            try {
                File tempFile = File.createTempFile("groovyc-files-", ".txt");
                temporaryFiles.add(tempFile);
                PrintWriter pw = printWriter(tempFile);
                for (File srcFile : compileList) {
                    pw.println(srcFile.getPath());
                }
                pw.close();
                commandLineList.add("@" + tempFile.getPath());
            } catch (IOException e) {
                log.error("Error creating file list", e);
            }
        } else {
            for (File srcFile : compileList) {
                commandLineList.add(srcFile.getPath());
            }
        }
    }

    @SuppressFBWarnings(value = "DM_DEFAULT_ENCODING", justification = "This is only used to store filenames when exceeding a particular length limit when in fork mode")
    private PrintWriter printWriter(File tempFile) throws IOException {
        // we could make a forkFileListEncoding but seems like a rare scenario
        return new PrintWriter(new FileWriter(tempFile));
    }

    private String[] makeCommandLine(List<String> commandLineList) {
        log.info("Compilation arguments:\n" + DefaultGroovyMethods.join((Iterable<String>) commandLineList, "\n"));
        return commandLineList.toArray(EMPTY_STRING_ARRAY);
    }

    private void runForked(String[] commandLine) {
        Execute executor = new Execute();
        executor.setAntRun(getProject());
        executor.setWorkingDirectory(getProject().getBaseDir());
        executor.setCommandline(commandLine);
        try {
            executor.execute();
        } catch (final IOException ioe) {
            throw new BuildException("Error running forked groovyc.", ioe);
        }
        int returnCode = executor.getExitValue();
        if (returnCode != 0) {
            taskSuccess = false;
            if (errorProperty != null) {
                getProject().setNewProperty(errorProperty, "true");
            }
            if (failOnError) {
                throw new BuildException("Forked groovyc returned error code: " + returnCode);
            } else {
                log.error("Forked groovyc returned error code: " + returnCode);
            }
        }
    }

    private void runCompiler(String[] commandLine) {
        // hand crank it so we can add our own compiler configuration
        try {
            FileSystemCompiler.CompilationOptions options = new FileSystemCompiler.CompilationOptions();
            CommandLine parser = FileSystemCompiler.configureParser(options);
            parser.parseArgs(commandLine);
            configuration = options.toCompilerConfiguration();
            configuration.setScriptExtensions(getScriptExtensions());
            String tmpExtension = getScriptExtension();
            if (tmpExtension.startsWith("*."))
                tmpExtension = tmpExtension.substring(1);
            configuration.setDefaultScriptExtension(tmpExtension);
            if (targetBytecode != null) {
                configuration.setTargetBytecode(targetBytecode);
            }

            // Load the file name list
            String[] fileNames = options.generateFileNames();
            boolean fileNameErrors = (fileNames == null || !FileSystemCompiler.validateFiles(fileNames));
            if (!fileNameErrors) {
                try (GroovyClassLoader loader = buildClassLoaderFor()) {
                    FileSystemCompiler.doCompilation(configuration, makeCompileUnit(loader), fileNames, forceLookupUnnamedFiles);
                }
            }
        } catch (Exception e) {
            Throwable t = e;
            if (e.getClass() == RuntimeException.class && e.getCause() != null) {
                // unwrap to the real exception
                t = e.getCause();
            }
            Writer writer = new StringBuilderWriter();
            new ErrorReporter(t, false).write(new PrintWriter(writer));
            String message = writer.toString();

            taskSuccess = false;
            if (errorProperty != null) {
                getProject().setNewProperty(errorProperty, "true");
            }

            if (failOnError) {
                log.error(message);
                throw new BuildException("Compilation Failed", t, getLocation());
            } else {
                log.error(message);
            }
        }
    }

    protected void compile() {
        if (compileList.length == 0) return;

        try {
            log.info("Compiling " + compileList.length + " source file"
                    + (compileList.length == 1 ? "" : "s")
                    + (destDir != null ? " to " + destDir : ""));

            listFiles();

            Path classpath = Optional.ofNullable(getClasspath()).orElse(new Path(getProject()));
            List<String> jointOptions = extractJointOptions(classpath);
            List<String> commandLineList = new ArrayList<>();

            if (fork) doForkCommandLineList(commandLineList, classpath, File.separator);
            doNormalCommandLineList(commandLineList, jointOptions, classpath);
            addSourceFiles(commandLineList);

            String[] commandLine = makeCommandLine(commandLineList);

            if (fork) {
                runForked(commandLine);
            } else {
                runCompiler(commandLine);
            }
        } finally {
            for (File temporaryFile : temporaryFiles) {
                try {
                    FileSystemCompiler.deleteRecursive(temporaryFile);
                } catch (Throwable t) {
                    System.err.println("error: could not delete temp files - " + temporaryFile.getPath());
                }
            }
        }
    }

    /**
     * @deprecated This method is not in use anymore. Use {@link Groovyc#makeCompileUnit(GroovyClassLoader)} instead.
     */
    @Deprecated
    protected CompilationUnit makeCompileUnit() {
        return makeCompileUnit(buildClassLoaderFor());
    }

    protected CompilationUnit makeCompileUnit(GroovyClassLoader loader) {
        Map<String, Object> options = configuration.getJointCompilationOptions();
        if (options != null) {
            if (keepStubs) {
                options.put("keepStubs", Boolean.TRUE);
            }
            if (stubDir != null) {
                options.put("stubDir", stubDir);
            } else {
                try {
                    File tempStubDir = DefaultGroovyStaticMethods.createTempDir(null, "groovy-generated-", "-java-source");
                    temporaryFiles.add(tempStubDir);
                    options.put("stubDir", tempStubDir);
                } catch (IOException ioe) {
                    throw new BuildException(ioe);
                }
            }
            return new JavaAwareCompilationUnit(configuration, loader);
        } else {
            return new CompilationUnit(configuration, null, loader);
        }
    }

    protected GroovyClassLoader buildClassLoaderFor() {
        if (fork) {
            throw new GroovyBugError("Cannot use Groovyc#buildClassLoaderFor() for forked compilation");
        }
        // GROOVY-5044
        if (!getIncludeantruntime()) {
            throw new IllegalArgumentException("The includeAntRuntime=false option is not compatible with fork=false");
        }

        ClassLoader loader = getClass().getClassLoader();
        if (loader instanceof AntClassLoader) {
            AntClassLoader antLoader = (AntClassLoader) loader;
            String[] pathElm = antLoader.getClasspath().split(File.pathSeparator, -1);
            List<String> classpath = configuration.getClasspath();
            /*
             * Iterate over the classpath provided to groovyc, and add any missing path
             * entries to the AntClassLoader.  This is a workaround, since for some reason
             * 'directory' classpath entries were not added to the 'AntClassLoader' classpath.
             */
            for (String cpEntry : classpath) {
                boolean found = false;
                for (String path : pathElm) {
                    if (cpEntry.equals(path)) {
                        found = true;
                        break;
                    }
                }
                /*
                 * fix for GROOVY-2284
                 * seems like AntClassLoader doesn't check if the file
                 * may not exist in the classpath yet
                 */
                if (!found && new File(cpEntry).exists()) {
                    try {
                        antLoader.addPathElement(cpEntry);
                    } catch (BuildException e) {
                        log.warn("The classpath entry " + cpEntry + " is not a valid Java resource");
                    }
                }
            }
        }

        @SuppressWarnings("removal") // TODO a future Groovy version should perform the operation not as a privileged action
        GroovyClassLoader groovyLoader = java.security.AccessController.doPrivileged((PrivilegedAction<GroovyClassLoader>) () ->
                new GroovyClassLoader(loader, configuration));

        if (!forceLookupUnnamedFiles) {
            // in normal case we don't need to do script lookups
            groovyLoader.setResourceLoader(filename -> null);
        }
        return groovyLoader;
    }

    private Set<String> getScriptExtensions() {
        return scriptExtensions;
    }

    private void loadRegisteredScriptExtensions() {
        if (scriptExtensions.isEmpty()) {
            scriptExtensions.add(getScriptExtension().substring(2)); // first extension will be the one set explicitly on <groovyc>

            Path classpath = Optional.ofNullable(getClasspath()).orElse(new Path(getProject()));
            try (GroovyClassLoader loader = new GroovyClassLoader(getClass().getClassLoader())) {
                for (String element : classpath.list()) {
                    loader.addClasspath(element);
                }
                scriptExtensions.addAll(SourceExtensionHandler.getRegisteredExtensions(loader));
            } catch (IOException e) {
                throw new RuntimeException(e);
            }
        }
    }
}
