/*
 *  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
 *
 *      https://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.taskdefs.launcher;

import static org.apache.tools.ant.MagicNames.ANT_SHELL_LAUNCHER_REF_ID;
import static org.apache.tools.ant.MagicNames.ANT_VM_LAUNCHER_REF_ID;

import java.io.File;
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.util.Optional;

import org.apache.tools.ant.Project;
import org.apache.tools.ant.taskdefs.condition.Os;
import org.apache.tools.ant.types.Commandline;
import org.apache.tools.ant.util.FileUtils;

/**
 * A command launcher for a particular JVM/OS platform. This class is
 * a general purpose command launcher which can only launch commands
 * in the current working directory.
 */
public class CommandLauncher {

    protected static final FileUtils FILE_UTILS = FileUtils.getFileUtils();

    private static CommandLauncher vmLauncher = null;
    private static CommandLauncher shellLauncher = null;

    static {
        if (!Os.isFamily("os/2")) {
            vmLauncher = new Java13CommandLauncher();
        }

        if (Os.isFamily("mac") && !Os.isFamily("unix")) {
            // Mac
            shellLauncher = new MacCommandLauncher(new CommandLauncher());
        } else if (Os.isFamily("os/2")) {
            // OS/2
            shellLauncher = new OS2CommandLauncher(new CommandLauncher());
        } else if (Os.isFamily("windows")) {
            CommandLauncher baseLauncher = new CommandLauncher();

            if (!Os.isFamily("win9x")) {
                // Windows XP/2000/NT
                shellLauncher = new WinNTCommandLauncher(baseLauncher);
            } else {
                // Windows 98/95 - need to use an auxiliary script
                shellLauncher =
                    new ScriptCommandLauncher("bin/antRun.bat", baseLauncher);
            }
        } else if (Os.isFamily("netware")) {

            CommandLauncher baseLauncher = new CommandLauncher();

            shellLauncher =
                new PerlScriptCommandLauncher("bin/antRun.pl", baseLauncher);
        } else if (Os.isFamily("openvms")) {
            // OpenVMS
            shellLauncher = new VmsCommandLauncher();
        } else {
            // Generic
            shellLauncher = new ScriptCommandLauncher("bin/antRun",
                new CommandLauncher());
        }
    }

    /**
     * Launches the given command in a new process.
     *
     * @param project
     *        The project that the command is part of.
     * @param cmd
     *        The command to execute.
     * @param env
     *        The environment for the new process. If null, the
     *        environment of the current process is used.
     * @return the created Process.
     * @throws IOException
     *         if attempting to run a command in a specific directory.
     */
    public Process exec(Project project, String[] cmd, String[] env)
        throws IOException {
        if (project != null) {
            project.log("Execute:CommandLauncher: "
                + Commandline.describeCommand(cmd), Project.MSG_DEBUG);
        }
        return Runtime.getRuntime().exec(cmd, env);
    }

    /**
     * Launches the given command in a new process, in the given
     * working directory.
     *
     * @param project
     *        The project that the command is part of.
     * @param cmd
     *        The command to execute.
     * @param env
     *        The environment for the new process. If null, the
     *        environment of the current process is used.
     * @param workingDir
     *        The directory to start the command in. If null, the
     *        current directory is used.
     * @return the created Process.
     * @throws IOException
     *         if trying to change directory.
     */
    public Process exec(Project project, String[] cmd, String[] env,
                        File workingDir) throws IOException {
        if (workingDir == null) {
            return exec(project, cmd, env);
        }
        throw new IOException(
            "Cannot execute a process in different directory under this JVM");
    }

    /**
     * Obtains the shell launcher configured for the given project or
     * the default shell launcher.
     *
     * @param project Project
     * @return CommandLauncher
     */
    public static CommandLauncher getShellLauncher(Project project) {
        CommandLauncher launcher = extractLauncher(ANT_SHELL_LAUNCHER_REF_ID,
                                                   project);
        if (launcher == null) {
            launcher = shellLauncher;
        }

        return launcher;
    }

    /**
     * Obtains the VM launcher configured for the given project or
     * the default VM launcher.
     *
     * @param project Project
     * @return CommandLauncher
     */
    public static CommandLauncher getVMLauncher(Project project) {
        CommandLauncher launcher = extractLauncher(ANT_VM_LAUNCHER_REF_ID,
                                                   project);
        if (launcher == null) {
            launcher = vmLauncher;
        }
        return launcher;
    }

    private static CommandLauncher extractLauncher(String referenceName,
                                                   Project project) {
        return Optional.ofNullable(project)
            .map(p -> p.<CommandLauncher> getReference(referenceName))
            .orElseGet(() -> getSystemLauncher(referenceName));
    }

    private static CommandLauncher getSystemLauncher(String launcherRefId) {
        String launcherClass = System.getProperty(launcherRefId);
        if (launcherClass != null) {
            try {
                return Class.forName(launcherClass).asSubclass(CommandLauncher.class)
                        .getDeclaredConstructor().newInstance();
            } catch (InstantiationException | IllegalAccessException
                    | ClassNotFoundException
                    | NoSuchMethodException | InvocationTargetException e) {
                System.err.println("Could not instantiate launcher class "
                    + launcherClass + ": " + e.getMessage());
            }
        }
        return null;
    }

    /**
     * Sets the VM launcher to use for the given project.
     *
     * @param project Project
     * @param launcher CommandLauncher
     */
    public static void setVMLauncher(Project project,
                                     CommandLauncher launcher) {
        if (project != null) {
            project.addReference(ANT_VM_LAUNCHER_REF_ID, launcher);
        }
    }

    /**
     * Sets the shell launcher to use for the given project.
     *
     * @param project Project
     * @param launcher CommandLauncher
     */
    public static void setShellLauncher(Project project,
                                        CommandLauncher launcher) {
        if (project != null) {
            project.addReference(ANT_SHELL_LAUNCHER_REF_ID, launcher);
        }
    }

}
