/*
 *  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.apache.tools.ant.types;

import java.util.Enumeration;
import java.util.LinkedList;
import java.util.List;
import java.util.ListIterator;
import java.util.Properties;
import java.util.Vector;

import org.apache.tools.ant.BuildException;
import org.apache.tools.ant.Project;
import org.apache.tools.ant.util.JavaEnvUtils;

/**
 * A representation of a Java command line that is
 * a composite of 2 <tt>Commandline</tt>s. One is used for the
 * vm/options and one for the classname/arguments. It provides
 * specific methods for a Java command line.
 *
 */
public class CommandlineJava implements Cloneable {

    /**
     * commands to the JVM
     */
    private Commandline vmCommand = new Commandline();
    /**
     * actual java commands
     */
    private Commandline javaCommand = new Commandline();
    /**
     * properties to add using -D
     */
    private SysProperties sysProperties = new SysProperties();
    private Path classpath = null;
    private Path bootclasspath = null;
    private Path modulepath = null;
    private Path upgrademodulepath = null;
    private String vmVersion;
    private String maxMemory = null;
    /**
     *  any assertions to make? Currently only supported in forked JVMs
     */
    private Assertions assertions = null;

    /**
     * Indicate whether it will execute a jar file, module or main class.
     * In this case of jar the first vm option must be a -jar and the 'executable' is a jar file.
     * In case of module the first vm option is -m and the 'executable' is 'module/mainClass'.
     */
     private ExecutableType executableType;

    /**
     * Whether system properties and bootclasspath shall be cloned.
     * @since Ant 1.7
     */
    private boolean cloneVm = false;

    /**
     * Specialized Environment class for System properties.
     */
    public static class SysProperties extends Environment implements Cloneable {
        // CheckStyle:VisibilityModifier OFF - bc
        /** the system properties. */
        Properties sys = null;
        // CheckStyle:VisibilityModifier ON
        private Vector<PropertySet> propertySets = new Vector<>();

        /**
         * Get the properties as an array; this is an override of the
         * superclass, as it evaluates all the properties.
         * @return the array of definitions; may be null.
         * @throws BuildException on error.
         */
        @Override
        public String[] getVariables() throws BuildException {

            List<String> definitions = new LinkedList<>();
            addDefinitionsToList(definitions.listIterator());
            if (definitions.isEmpty()) {
                return null;
            }
            return definitions.toArray(new String[definitions.size()]);
        }

        /**
         * Add all definitions (including property sets) to a list.
         * @param listIt list iterator supporting add method.
         */
        public void addDefinitionsToList(ListIterator<String> listIt) {
            String[] props = super.getVariables();
            if (props != null) {
                for (int i = 0; i < props.length; i++) {
                    listIt.add("-D" + props[i]);
                }
            }
            Properties propertySetProperties = mergePropertySets();
            for (Enumeration<?> e = propertySetProperties.keys();
                 e.hasMoreElements();) {
                String key = (String) e.nextElement();
                String value = propertySetProperties.getProperty(key);
                listIt.add("-D" + key + "=" + value);
            }
        }

        /**
         * Get the size of the sysproperties instance. This merges all
         * property sets, so is not an O(1) operation.
         * @return the size of the sysproperties instance.
         */
        public int size() {
            Properties p = mergePropertySets();
            return variables.size() + p.size();
        }

        /**
         * Cache the system properties and set the system properties to the
         * new values.
         * @throws BuildException if Security prevented this operation.
         */
        public void setSystem() throws BuildException {
            try {
                sys = System.getProperties();
                Properties p = new Properties();
                for (Enumeration<?> e = sys.propertyNames(); e.hasMoreElements();) {
                    String name = (String) e.nextElement();
                    String value = sys.getProperty(name);
                    if (name != null && value != null) {
                        p.put(name, value);
                    }
                }
                p.putAll(mergePropertySets());
                for (Environment.Variable v : variables) {
                    v.validate();
                    p.put(v.getKey(), v.getValue());
                }
                System.setProperties(p);
            } catch (SecurityException e) {
                throw new BuildException("Cannot modify system properties", e);
            }
        }

        /**
         * Restore the system properties to the cached value.
         * @throws BuildException  if Security prevented this operation, or
         * there were no system properties to restore.
         */
        public void restoreSystem() throws BuildException {
            if (sys == null) {
                throw new BuildException("Unbalanced nesting of SysProperties");
            }

            try {
                System.setProperties(sys);
                sys = null;
            } catch (SecurityException e) {
                throw new BuildException("Cannot modify system properties", e);
            }
        }

        /**
         * Create a deep clone.
         * @return a cloned instance of SysProperties.
         * @exception CloneNotSupportedException for signature.
         */
        @SuppressWarnings("unchecked")
        @Override
        public Object clone() throws CloneNotSupportedException {
            try {
                SysProperties c = (SysProperties) super.clone();
                c.variables = (Vector<Environment.Variable>) variables.clone();
                c.propertySets = (Vector<PropertySet>) propertySets.clone();
                return c;
            } catch (CloneNotSupportedException e) {
                return null;
            }
        }

        /**
         * Add a propertyset to the total set.
         * @param ps the new property set.
         */
        public void addSyspropertyset(PropertySet ps) {
            propertySets.addElement(ps);
        }

        /**
         * Add a propertyset to the total set.
         * @param ps the new property set.
         * @since Ant 1.6.3
         */
        public void addSysproperties(SysProperties ps) {
            variables.addAll(ps.variables);
            propertySets.addAll(ps.propertySets);
        }

        /**
         * Merge all property sets into a single Properties object.
         * @return the merged object.
         */
        private Properties mergePropertySets() {
            Properties p = new Properties();
            for (PropertySet ps : propertySets) {
                p.putAll(ps.getProperties());
            }
            return p;
        }

    }

    /**
     * Constructor uses the VM we are running on now.
     */
    public CommandlineJava() {
        setVm(JavaEnvUtils.getJreExecutable("java"));
        setVmversion(JavaEnvUtils.getJavaVersion());
    }

    /**
     * Create a new argument to the java program.
     * @return an argument to be configured.
     */
    public Commandline.Argument createArgument() {
        return javaCommand.createArgument();
    }

    /**
     * Create a new JVM argument.
     * @return an argument to be configured.
     */
    public Commandline.Argument createVmArgument() {
        return vmCommand.createArgument();
    }

    /**
     * Add a system property.
     * @param sysp a property to be set in the JVM.
     */
    public void addSysproperty(Environment.Variable sysp) {
        sysProperties.addVariable(sysp);
    }

    /**
     * Add a set of system properties.
     * @param sysp a set of properties.
     */
    public void addSyspropertyset(PropertySet sysp) {
        sysProperties.addSyspropertyset(sysp);
    }

    /**
     * Add a set of system properties.
     * @param sysp a set of properties.
     * @since Ant 1.6.3
     */
    public void addSysproperties(SysProperties sysp) {
        sysProperties.addSysproperties(sysp);
    }

    /**
     * Set the executable used to start the new JVM.
     * @param vm the executable to use.
     */
    public void setVm(String vm) {
        vmCommand.setExecutable(vm);
    }

    /**
     * Set the JVM version required.
     * @param value the version required.
     */
    public void setVmversion(String value) {
        vmVersion = value;
    }

    /**
     * Set whether system properties will be copied to the cloned VM--as
     * well as the bootclasspath unless you have explicitly specified
     * a bootclasspath.
     * @param cloneVm if true copy the system properties.
     * @since Ant 1.7
     */
    public void setCloneVm(boolean cloneVm) {
        this.cloneVm = cloneVm;
    }

    /**
     * Get the current assertions.
     * @return assertions or null.
     */
    public Assertions getAssertions() {
        return assertions;
    }

    /**
     * Add an assertion set to the command.
     * @param assertions assertions to make.
     */
    public void setAssertions(Assertions assertions) {
        this.assertions = assertions;
    }

    /**
     * Set a jar file to execute via the -jar option.
     * @param jarpathname the pathname of the jar to execute.
     */
    public void setJar(String jarpathname) {
        javaCommand.setExecutable(jarpathname);
        executableType = ExecutableType.JAR;
    }

    /**
     * Get the name of the jar to be run.
     * @return the pathname of the jar file to run via -jar option
     * or <tt>null</tt> if there is no jar to run.
     * @see #getClassname()
     */
    public String getJar() {
        if (executableType == ExecutableType.JAR) {
            return javaCommand.getExecutable();
        }
        return null;
    }

    /**
     * Set the classname to execute.
     * @param classname the fully qualified classname.
     */
    public void setClassname(String classname) {
        if (executableType == ExecutableType.MODULE) {
            javaCommand.setExecutable(createModuleClassPair(
                    parseModuleFromModuleClassPair(javaCommand.getExecutable()),
                    classname), false);
        } else {
            javaCommand.setExecutable(classname);
            executableType = ExecutableType.CLASS;
        }
    }

    /**
     * Get the name of the class to be run.
     * @return the name of the class to run or <tt>null</tt> if there is no class.
     * @see #getJar()
     */
    public String getClassname() {
        if (executableType != null) {
            switch (executableType) {
                case CLASS:
                    return javaCommand.getExecutable();
                case MODULE:
                    return parseClassFromModuleClassPair(javaCommand.getExecutable());
                default:
            }
        }
        return null;
    }

    /**
     * Set the module to execute.
     * @param module  the module name.
     * @since 1.9.7
     */
    public void setModule(final String module) {
        if (executableType == null) {
            javaCommand.setExecutable(module);
        } else {
            switch (executableType) {
                case JAR:
                    javaCommand.setExecutable(module, false);
                    break;
                case CLASS:
                    javaCommand.setExecutable(createModuleClassPair(module,
                            javaCommand.getExecutable()), false);
                    break;
                case MODULE:
                    javaCommand.setExecutable(createModuleClassPair(module,
                            parseClassFromModuleClassPair(javaCommand.getExecutable())), false);
                    break;
                default:
            }
        }
        executableType = ExecutableType.MODULE;
    }

    /**
     * Get the name of the module to be run.
     * @return the name of the module to run or <tt>null</tt> if there is no module.
     * @see #getJar()
     * @see #getClassname()
     * @since 1.9.7
     */
    public String getModule() {
        if (executableType == ExecutableType.MODULE) {
            return parseModuleFromModuleClassPair(javaCommand.getExecutable());
        }
        return null;
    }

    /**
     * Create a classpath.
     * @param p the project to use to create the path.
     * @return a path to be configured.
     */
    public Path createClasspath(Project p) {
        if (classpath == null) {
            classpath = new Path(p);
        }
        return classpath;
    }

    /**
     * Create a boot classpath.
     * @param p the project to use to create the path.
     * @return a path to be configured.
     * @since Ant 1.6
     */
    public Path createBootclasspath(Project p) {
        if (bootclasspath == null) {
            bootclasspath = new Path(p);
        }
        return bootclasspath;
    }

    /**
     * Create a modulepath.
     * @param p the project to use to create the path.
     * @return a path to be configured.
     * @since 1.9.7
     */
    public Path createModulepath(Project p) {
        if (modulepath == null) {
            modulepath = new Path(p);
        }
        return modulepath;
    }

    /**
     * Create an upgrademodulepath.
     * @param p the project to use to create the path.
     * @return a path to be configured.
     * @since 1.9.7
     */
    public Path createUpgrademodulepath(Project p) {
        if (upgrademodulepath == null) {
            upgrademodulepath = new Path(p);
        }
        return upgrademodulepath;
    }

    /**
     * Get the vm version.
     * @return the vm version.
     */
    public String getVmversion() {
        return vmVersion;
    }

    /**
     * Get the command line to run a Java vm.
     * @return the list of all arguments necessary to run the vm.
     */
    public String[] getCommandline() {
        //create the list
        List<String> commands = new LinkedList<>();
        //fill it
        addCommandsToList(commands.listIterator());
        //convert to an array
        return commands.toArray(new String[commands.size()]);
    }

    /**
     * Add all the commands to a list identified by the iterator passed in.
     * @param listIterator an iterator that supports the add method.
     * @since Ant 1.6
     */
    private void addCommandsToList(final ListIterator<String> listIterator) {
        //create the command to run Java, including user specified options
        getActualVMCommand().addCommandToList(listIterator);
        // properties are part of the vm options...
        sysProperties.addDefinitionsToList(listIterator);

        if (isCloneVm()) {
            SysProperties clonedSysProperties = new SysProperties();
            PropertySet ps = new PropertySet();
            PropertySet.BuiltinPropertySetName sys = new PropertySet.BuiltinPropertySetName();
            sys.setValue("system");
            ps.appendBuiltin(sys);
            clonedSysProperties.addSyspropertyset(ps);
            clonedSysProperties.addDefinitionsToList(listIterator);
        }
        //boot classpath
        Path bcp = calculateBootclasspath(true);
        if (bcp.size() > 0) {
            listIterator.add("-Xbootclasspath:" + bcp.toString());
        }
        //main classpath
        if (haveClasspath()) {
            listIterator.add("-classpath");
            listIterator.add(classpath.concatSystemClasspath("ignore").toString());
        }
        //module path
        if (haveModulepath()) {
            listIterator.add("--module-path");
            listIterator.add(modulepath.concatSystemClasspath("ignore").toString());
        }
        //upgrade module path
        if (haveUpgrademodulepath()) {
            listIterator.add("--upgrade-module-path");
            listIterator.add(upgrademodulepath.concatSystemClasspath("ignore").toString());
        }
        //now any assertions are added
        if (getAssertions() != null) {
            getAssertions().applyAssertions(listIterator);
        }
        // JDK usage command line says that -jar must be the first option, as there is
        // a bug in JDK < 1.4 that forces the jvm type to be specified as the first
        // option, it is appended here as specified in the docs even though there is
        // in fact no order.
        if (executableType == ExecutableType.JAR) {
            listIterator.add("-jar");
        } else if (executableType == ExecutableType.MODULE) {
            listIterator.add("-m");
        }
        // this is the classname to run as well as its arguments.
        // in case of ExecutableType.JAR, the executable is a jar file,
        // in case of ExecutableType.MODULE, the executable is a module name, portentially including a class name.
        javaCommand.addCommandToList(listIterator);
    }

    /**
     * Specify max memory of the JVM.
     * -mx or -Xmx depending on VM version.
     * @param max the string to pass to the jvm to specify the max memory.
     */
    public void setMaxmemory(String max) {
        this.maxMemory = max;
    }

    /**
     * Get a string description.
     * @return the command line as a string.
     */
    @Override
    public String toString() {
        return Commandline.toString(getCommandline());
    }

    /**
     * Return a String that describes the command and arguments suitable for
     * verbose output before a call to <code>Runtime.exec(String[])</code>.
     * @return the description string.
     * @since Ant 1.5
     */
    public String describeCommand() {
        return Commandline.describeCommand(getCommandline());
    }

    /**
     * Return a String that describes the java command and arguments
     * for in-VM executions.
     *
     * <p>The class name is the executable in this context.</p>
     * @return the description string.
     * @since Ant 1.5
     */
    public String describeJavaCommand() {
        return Commandline.describeCommand(getJavaCommand());
    }

    /**
     * Get the VM command parameters, including memory settings.
     * @return the VM command parameters.
     */
    protected Commandline getActualVMCommand() {
        Commandline actualVMCommand = (Commandline) vmCommand.clone();
        if (maxMemory != null) {
            if (vmVersion.startsWith("1.1")) {
                actualVMCommand.createArgument().setValue("-mx" + maxMemory);
            } else {
                actualVMCommand.createArgument().setValue("-Xmx" + maxMemory);
            }
        }
        return actualVMCommand;
    }

    /**
     * Get the size of the java command line. This is a fairly intensive
     * operation, as it has to evaluate the size of many components.
     * @return the total number of arguments in the java command line.
     * @see #getCommandline()
     * @deprecated since 1.7.
     *             Please dont use this, it effectively creates the
     *             entire command.
     */
    @Deprecated
    public int size() {
        int size = getActualVMCommand().size() + javaCommand.size()
            + sysProperties.size();
        // cloned system properties
        if (isCloneVm()) {
            size += System.getProperties().size();
        }
        // classpath is "-classpath <classpath>" -> 2 args
        if (haveClasspath()) {
            size += 2;
        }
        // bootclasspath is "-Xbootclasspath:<classpath>" -> 1 arg
        if (calculateBootclasspath(true).size() > 0) {
            size++;
        }
        // jar execution requires an additional -jar option
        if (executableType == ExecutableType.JAR || executableType == ExecutableType.MODULE) {
            size++;
        }
        //assertions take up space too
        if (getAssertions() != null) {
            size += getAssertions().size();
        }
        return size;
    }

    /**
     * Get the Java command to be used.
     * @return the java command--not a clone.
     */
    public Commandline getJavaCommand() {
        return javaCommand;
    }

    /**
     * Get the VM command, including memory.
     * @return A deep clone of the instance's VM command, with memory settings added.
     */
    public Commandline getVmCommand() {
        return getActualVMCommand();
    }

    /**
     * Get the classpath for the command.
     * @return the classpath or null.
     */
    public Path getClasspath() {
        return classpath;
    }

    /**
     * Get the boot classpath.
     * @return boot classpath or null.
     */
    public Path getBootclasspath() {
        return bootclasspath;
    }

    /**
     * Get the modulepath.
     * @return modulepath or null.
     * @since 1.9.7
     */
    public Path getModulepath() {
        return modulepath;
    }

    /**
     * Get the upgrademodulepath.
     * @return upgrademodulepath or null.
     * @since 1.9.7
     */
    public Path getUpgrademodulepath() {
        return upgrademodulepath;
    }

    /**
     * Cache current system properties and set them to those in this
     * Java command.
     * @throws BuildException  if Security prevented this operation.
     */
    public void setSystemProperties() throws BuildException {
        sysProperties.setSystem();
    }

    /**
     * Restore the cached system properties.
     * @throws BuildException  if Security prevented this operation, or
     * there was no system properties to restore
     */
    public void restoreSystemProperties() throws BuildException {
        sysProperties.restoreSystem();
    }

    /**
     * Get the system properties object.
     * @return The system properties object.
     */
    public SysProperties getSystemProperties() {
        return sysProperties;
    }

    /**
     * Deep clone the object.
     * @return a CommandlineJava object.
     * @throws BuildException if anything went wrong.
     * @throws CloneNotSupportedException never.
     */
    @Override
    public Object clone() throws CloneNotSupportedException {
        try {
            CommandlineJava c = (CommandlineJava) super.clone();
            c.vmCommand = (Commandline) vmCommand.clone();
            c.javaCommand = (Commandline) javaCommand.clone();
            c.sysProperties = (SysProperties) sysProperties.clone();
            if (classpath != null) {
                c.classpath = (Path) classpath.clone();
            }
            if (bootclasspath != null) {
                c.bootclasspath = (Path) bootclasspath.clone();
            }
            if (modulepath != null) {
                c.modulepath = (Path) modulepath.clone();
            }
            if (upgrademodulepath != null) {
                c.upgrademodulepath = (Path) upgrademodulepath.clone();
            }
            if (assertions != null) {
                c.assertions = (Assertions) assertions.clone();
            }
            return c;
        } catch (CloneNotSupportedException e) {
            throw new BuildException(e);
        }
    }

    /**
     * Clear out the java arguments.
     */
    public void clearJavaArgs() {
        javaCommand.clearArgs();
    }

    /**
     * Determine whether the classpath has been specified, and whether it shall
     * really be used or be nulled by build.sysclasspath.
     * @return true if the classpath is to be used.
     * @since Ant 1.6
     */
    public boolean haveClasspath() {
        Path fullClasspath = classpath != null ? classpath.concatSystemClasspath("ignore") : null;
        return fullClasspath != null && fullClasspath.toString().trim().length() > 0;
    }

    /**
     * Determine whether the bootclasspath has been specified, and whether it
     * shall really be used (build.sysclasspath could be set or the VM may not
     * support it).
     *
     * @param log whether to log a warning if a bootclasspath has been
     * specified but will be ignored.
     * @return true if the bootclasspath is to be used.
     * @since Ant 1.6
     */
    protected boolean haveBootclasspath(boolean log) {
        return calculateBootclasspath(log).size() > 0;
    }

    /**
     * Determine whether the modulepath has been specified.
     * @return true if the modulepath is to be used.
     * @since 1.9.7
     */
    public boolean haveModulepath() {
        Path fullClasspath = modulepath != null
                ? modulepath.concatSystemClasspath("ignore") : null;
        return fullClasspath != null
            && !fullClasspath.toString().trim().isEmpty();
    }

    /**
     * Determine whether the upgrademodulepath has been specified.
     * @return true if the upgrademodulepath is to be used.
     * @since 1.9.7
     */
    public boolean haveUpgrademodulepath() {
        Path fullClasspath = upgrademodulepath != null
                ? upgrademodulepath.concatSystemClasspath("ignore") : null;
        return fullClasspath != null && !fullClasspath.toString().trim().isEmpty();
    }

    /**
     * Calculate the bootclasspath based on the bootclasspath
     * specified, the build.sysclasspath and ant.build.clonevm magic
     * properties as well as the cloneVm attribute.
     * @param log whether to write messages to the log.
     * @since Ant 1.7
     */
    private Path calculateBootclasspath(boolean log) {
        if (vmVersion.startsWith("1.1")) {
            if (bootclasspath != null && log) {
                bootclasspath.log("Ignoring bootclasspath as the target VM doesn't support it.");
            }
        } else {
            Path b = bootclasspath;
            if (b == null) {
                b = new Path(null);
            }
            // even with no user-supplied bootclasspath
            // build.sysclasspath could be set to something other than
            // "ignore" and thus create one
            return b.concatSystemBootClasspath(isCloneVm() ? "last" : "ignore");
        }
        return new Path(null);
    }

    /**
     * Find out whether either of the cloneVm attribute or the magic property
     * ant.build.clonevm has been set.
     * @return <code>boolean</code>.
     * @since 1.7
     */
    private boolean isCloneVm() {
        return cloneVm || Boolean.parseBoolean(System.getProperty("ant.build.clonevm"));
    }

    /**
     * Creates JDK 9 main module command line argument.
     * @param module the module name.
     * @param classname the classname or <code>null</code>.
     * @return the main module with optional classname command line argument.
     * @since 1.9.7
     */
    private static String createModuleClassPair(final String module, final String classname) {
        return classname == null ? module : String.format("%s/%s", module, classname);   //NOI18N
    }

    /**
     * Parses a module name from JDK 9 main module command line argument.
     * @param moduleClassPair a module with optional classname or <code>null</code>.
     * @return the module name or <code>null</code>.
     * @since 1.9.7
     */
    private static String parseModuleFromModuleClassPair(final String moduleClassPair) {
        if (moduleClassPair == null) {
            return null;
        }
        final String[] moduleAndClass = moduleClassPair.split("/");  //NOI18N
        return moduleAndClass[0];
    }

    /**
     * Parses a classname from JDK 9 main module command line argument.
     * @param moduleClassPair a module with optional classname or <code>null</code>.
     * @return the classname or <code>null</code>.
     * @since 1.9.7
     */
    private static String parseClassFromModuleClassPair(final String moduleClassPair) {
        if (moduleClassPair == null) {
            return null;
        }
        final String[] moduleAndClass = moduleClassPair.split("/");  //NOI18N
        return moduleAndClass.length == 2 ? moduleAndClass[1] : null;
    }

    /**
     * Type of execution.
     * @since 1.9.7
     */
    private enum ExecutableType {
        /**
         * Main class execution.
         */
        CLASS,
        /**
         * Jar file execution.
         */
        JAR,
        /**
         * Module execution.
         */
        MODULE
    }
}
