/*
 * 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.maven.shared.invoker;

import java.io.File;
import java.io.IOException;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Properties;

import org.apache.maven.shared.invoker.InvocationRequest.CheckSumPolicy;
import org.apache.maven.shared.invoker.InvocationRequest.ReactorFailureBehavior;
import org.apache.maven.shared.utils.Os;
import org.apache.maven.shared.utils.StringUtils;
import org.apache.maven.shared.utils.cli.CommandLineException;
import org.apache.maven.shared.utils.cli.Commandline;

/**
 * <p>MavenCommandLineBuilder class.</p>
 */
public class MavenCommandLineBuilder {

    private static final InvokerLogger DEFAULT_LOGGER = new SystemOutLogger();

    private InvokerLogger logger = DEFAULT_LOGGER;

    private File baseDirectory;

    private File localRepositoryDirectory;

    private File mavenHome;

    private File mavenExecutable;

    /**
     * <p>build.</p>
     *
     * @param request a {@link org.apache.maven.shared.invoker.InvocationRequest} object.
     * @return a {@link org.apache.maven.shared.utils.cli.Commandline} object.
     * @throws org.apache.maven.shared.invoker.CommandLineConfigurationException if any.
     */
    public Commandline build(InvocationRequest request) throws CommandLineConfigurationException {

        Commandline cli = new Commandline();

        setupMavenHome(request);

        // discover value for working directory
        setupBaseDirectory(request);
        cli.setWorkingDirectory(baseDirectory);

        checkRequiredState();

        setupMavenExecutable(request);
        cli.setExecutable(mavenExecutable.getAbsolutePath());

        // handling for OS-level envars
        setShellEnvironment(request, cli);

        // interactive, offline, update-snapshots,
        // debug/show-errors, checksum policy
        setFlags(request, cli);

        // failure behavior and [eventually] forced-reactor
        // includes/excludes, etc.
        setReactorBehavior(request, cli);

        // local repository location
        setLocalRepository(request, cli);

        // pom-file handling
        setPomLocation(request, cli);

        setSettingsLocation(request, cli);

        setToolchainsLocation(request, cli);

        setProperties(request, cli);

        setProfiles(request, cli);

        setGoals(request, cli);

        setThreads(request, cli);

        setArgs(request, cli);

        return cli;
    }

    /**
     * <p>checkRequiredState.</p>
     */
    protected void checkRequiredState() {
        if (logger == null) {
            throw new IllegalStateException("A logger instance is required.");
        }
    }

    /**
     * <p>setSettingsLocation.</p>
     *
     * @param request a {@link org.apache.maven.shared.invoker.InvocationRequest} object.
     * @param cli a {@link org.apache.maven.shared.utils.cli.Commandline} object.
     */
    protected void setSettingsLocation(InvocationRequest request, Commandline cli) {
        File userSettingsFile = request.getUserSettingsFile();

        if (userSettingsFile != null) {
            try {
                userSettingsFile = userSettingsFile.getCanonicalFile();
            } catch (IOException e) {
                logger.debug(
                        "Failed to canonicalize user settings path: " + userSettingsFile.getAbsolutePath()
                                + ". Using as-is.",
                        e);
            }

            cli.createArg().setValue("-s");
            cli.createArg().setValue(userSettingsFile.getPath());
        }

        File globalSettingsFile = request.getGlobalSettingsFile();

        if (globalSettingsFile != null) {
            try {
                globalSettingsFile = globalSettingsFile.getCanonicalFile();
            } catch (IOException e) {
                logger.debug(
                        "Failed to canonicalize global settings path: " + globalSettingsFile.getAbsolutePath()
                                + ". Using as-is.",
                        e);
            }

            cli.createArg().setValue("-gs");
            cli.createArg().setValue(globalSettingsFile.getPath());
        }
    }

    /**
     * <p>setToolchainsLocation.</p>
     *
     * @param request a {@link org.apache.maven.shared.invoker.InvocationRequest} object.
     * @param cli a {@link org.apache.maven.shared.utils.cli.Commandline} object.
     */
    protected void setToolchainsLocation(InvocationRequest request, Commandline cli) {
        File toolchainsFile = request.getToolchainsFile();

        if (toolchainsFile != null) {
            try {
                toolchainsFile = toolchainsFile.getCanonicalFile();
            } catch (IOException e) {
                logger.debug(
                        "Failed to canonicalize toolchains path: " + toolchainsFile.getAbsolutePath()
                                + ". Using as-is.",
                        e);
            }

            cli.createArg().setValue("-t");
            cli.createArg().setValue(toolchainsFile.getPath());
        }
    }

    /**
     * <p>setShellEnvironment.</p>
     *
     * @param request a {@link org.apache.maven.shared.invoker.InvocationRequest} object.
     * @param cli a {@link org.apache.maven.shared.utils.cli.Commandline} object.
     */
    protected void setShellEnvironment(InvocationRequest request, Commandline cli) {
        cli.setShellEnvironmentInherited(request.isShellEnvironmentInherited());

        if (request.getJavaHome() != null) {
            cli.addEnvironment("JAVA_HOME", request.getJavaHome().getAbsolutePath());
        }

        if (request.getMavenOpts() != null) {
            cli.addEnvironment("MAVEN_OPTS", request.getMavenOpts());
        }

        for (Map.Entry<String, String> entry : request.getShellEnvironments().entrySet()) {
            cli.addEnvironment(entry.getKey(), entry.getValue());
        }
    }

    /**
     * <p>setProfiles.</p>
     *
     * @param request a {@link org.apache.maven.shared.invoker.InvocationRequest} object.
     * @param cli a {@link org.apache.maven.shared.utils.cli.Commandline} object.
     */
    protected void setProfiles(InvocationRequest request, Commandline cli) {
        List<String> profiles = request.getProfiles();

        if ((profiles != null) && !profiles.isEmpty()) {
            cli.createArg().setValue("-P");
            cli.createArg().setValue(String.join(",", profiles));
        }
    }

    /**
     * <p>setGoals.</p>
     *
     * @param request a {@link org.apache.maven.shared.invoker.InvocationRequest} object.
     * @param cli a {@link org.apache.maven.shared.utils.cli.Commandline} object.
     * @throws org.apache.maven.shared.invoker.CommandLineConfigurationException if any.
     */
    protected void setGoals(InvocationRequest request, Commandline cli) throws CommandLineConfigurationException {
        List<String> goals = request.getGoals();

        if ((goals != null) && !goals.isEmpty()) {
            try {
                cli.createArg().setLine(String.join(" ", goals));
            } catch (CommandLineException e) {
                throw new CommandLineConfigurationException("Problem setting goals", e);
            }
        }
    }

    /**
     * <p>setProperties.</p>
     *
     * @param request a {@link org.apache.maven.shared.invoker.InvocationRequest} object.
     * @param cli a {@link org.apache.maven.shared.utils.cli.Commandline} object.
     */
    protected void setProperties(InvocationRequest request, Commandline cli) {
        Properties properties = request.getProperties();

        if (properties != null) {
            for (Entry<Object, Object> entry : properties.entrySet()) {
                String key = (String) entry.getKey();
                String value = (String) entry.getValue();

                cli.createArg().setValue("-D");
                cli.createArg().setValue(key + '=' + value);
            }
        }
    }

    /**
     * <p>setPomLocation.</p>
     *
     * @param request a {@link org.apache.maven.shared.invoker.InvocationRequest} object.
     * @param cli a {@link org.apache.maven.shared.utils.cli.Commandline} object.
     */
    protected void setPomLocation(InvocationRequest request, Commandline cli) {
        File pom = request.getPomFile();
        String pomFilename = request.getPomFileName();

        if (pom == null) {
            if (pomFilename != null) {
                pom = new File(baseDirectory, pomFilename);
            } else {
                pom = new File(baseDirectory, "pom.xml");
            }
        }

        try {
            pom = pom.getCanonicalFile();
        } catch (IOException e) {
            logger.debug("Failed to canonicalize the POM path: " + pom + ". Using as-is.", e);
        }

        if (pom.getParentFile().equals(baseDirectory)) {
            // pom in project workspace
            if (!"pom.xml".equals(pom.getName())) {
                logger.debug("Specified POM file is not named 'pom.xml'. "
                        + "Using the '-f' command-line option to accommodate non-standard filename...");

                cli.createArg().setValue("-f");
                cli.createArg().setValue(pom.getName());
            }
        } else {
            cli.createArg().setValue("-f");
            cli.createArg().setValue(pom.getPath());
        }
    }

    void setupBaseDirectory(InvocationRequest request) {
        File baseDirectoryFromRequest = null;
        if (request.getBaseDirectory() != null) {
            baseDirectoryFromRequest = request.getBaseDirectory();
        } else {
            File pomFile = request.getPomFile();
            if (pomFile != null) {
                baseDirectoryFromRequest = pomFile.getParentFile();
            }
        }

        if (baseDirectoryFromRequest != null) {
            baseDirectory = baseDirectoryFromRequest;
        }

        if (baseDirectory == null) {
            baseDirectory = new File(System.getProperty("user.dir"));
        } else if (baseDirectory.isFile()) {
            logger.warn(
                    "Specified base directory (" + baseDirectory + ") is a file." + " Using its parent directory...");

            baseDirectory = baseDirectory.getParentFile();
        }

        try {
            baseDirectory = baseDirectory.getCanonicalFile();
        } catch (IOException e) {
            logger.debug("Failed to canonicalize base directory: " + baseDirectory + ". Using as-is.", e);
        }
    }

    /**
     * <p>setLocalRepository.</p>
     *
     * @param request a {@link org.apache.maven.shared.invoker.InvocationRequest} object.
     * @param cli a {@link org.apache.maven.shared.utils.cli.Commandline} object.
     */
    protected void setLocalRepository(InvocationRequest request, Commandline cli) {
        File localRepositoryDirectory = request.getLocalRepositoryDirectory(this.localRepositoryDirectory);

        if (localRepositoryDirectory != null) {
            try {
                localRepositoryDirectory = localRepositoryDirectory.getCanonicalFile();
            } catch (IOException e) {
                logger.debug(
                        "Failed to canonicalize local repository directory: " + localRepositoryDirectory
                                + ". Using as-is.",
                        e);
            }

            if (!localRepositoryDirectory.isDirectory()) {
                throw new IllegalArgumentException(
                        "Local repository location: '" + localRepositoryDirectory + "' is NOT a directory.");
            }

            cli.createArg().setValue("-D");
            cli.createArg().setValue("maven.repo.local=" + localRepositoryDirectory.getPath());
        }
    }

    /**
     * <p>setReactorBehavior.</p>
     *
     * @param request a {@link org.apache.maven.shared.invoker.InvocationRequest} object.
     * @param cli a {@link org.apache.maven.shared.utils.cli.Commandline} object.
     */
    protected void setReactorBehavior(InvocationRequest request, Commandline cli) {
        // NOTE: The default is "fail-fast"
        ReactorFailureBehavior failureBehavior = request.getReactorFailureBehavior();

        if (failureBehavior != null) {
            if (ReactorFailureBehavior.FailAtEnd.equals(failureBehavior)) {
                cli.createArg().setValue("-" + ReactorFailureBehavior.FailAtEnd.getShortOption());
            } else if (ReactorFailureBehavior.FailNever.equals(failureBehavior)) {
                cli.createArg().setValue("-" + ReactorFailureBehavior.FailNever.getShortOption());
            }
        }

        if (StringUtils.isNotEmpty(request.getResumeFrom())) {
            cli.createArg().setValue("-rf");
            cli.createArg().setValue(request.getResumeFrom());
        }

        List<String> projectList = request.getProjects();
        if (projectList != null) {
            cli.createArg().setValue("-pl");
            cli.createArg().setValue(String.join(",", projectList));

            if (request.isAlsoMake()) {
                cli.createArg().setValue("-am");
            }

            if (request.isAlsoMakeDependents()) {
                cli.createArg().setValue("-amd");
            }
        }
    }

    /**
     * <p>setFlags.</p>
     *
     * @param request a {@link org.apache.maven.shared.invoker.InvocationRequest} object.
     * @param cli a {@link org.apache.maven.shared.utils.cli.Commandline} object.
     */
    protected void setFlags(InvocationRequest request, Commandline cli) {
        if (request.isBatchMode()) {
            cli.createArg().setValue("-B");
        }

        if (request.isOffline()) {
            cli.createArg().setValue("-o");
        }

        if (request.getUpdateSnapshotsPolicy() == UpdateSnapshotsPolicy.ALWAYS) {
            cli.createArg().setValue("-U");
        }

        if (request.getUpdateSnapshotsPolicy() == UpdateSnapshotsPolicy.NEVER) {
            cli.createArg().setValue("-nsu");
        }

        if (!request.isRecursive()) {
            cli.createArg().setValue("-N");
        }

        if (request.isDebug()) {
            cli.createArg().setValue("-X");
        }
        // this is superseded by -X, if it exists.
        else if (request.isShowErrors()) {
            cli.createArg().setValue("-e");
        }

        CheckSumPolicy checksumPolicy = request.getGlobalChecksumPolicy();
        if (CheckSumPolicy.Fail.equals(checksumPolicy)) {
            cli.createArg().setValue("-C");
        } else if (CheckSumPolicy.Warn.equals(checksumPolicy)) {
            cli.createArg().setValue("-c");
        }

        if (request.isNonPluginUpdates()) {
            cli.createArg().setValue("-npu");
        }

        if (request.isShowVersion()) {
            cli.createArg().setValue("-V");
        }

        if (request.getBuilder() != null) {
            cli.createArg().setValue("-b");
            cli.createArg().setValue(request.getBuilder());
        }

        if (request.isQuiet()) {
            cli.createArg().setValue("-q");
        }

        if (request.isNoTransferProgress()) {
            cli.createArg().setValue("-ntp");
        }
    }

    /**
     * <p>setThreads.</p>
     *
     * @param request a {@link org.apache.maven.shared.invoker.InvocationRequest} object.
     * @param cli a {@link org.apache.maven.shared.utils.cli.Commandline} object.
     */
    protected void setThreads(InvocationRequest request, Commandline cli) {
        String threads = request.getThreads();
        if (threads != null && !threads.isEmpty()) {
            cli.createArg().setValue("-T");
            cli.createArg().setValue(threads);
        }
    }

    protected void setArgs(InvocationRequest request, Commandline cli) {
        for (String arg : request.getArgs()) {
            cli.createArg().setValue(arg);
        }
    }

    private void setupMavenHome(InvocationRequest request) {
        if (request.getMavenHome() != null) {
            mavenHome = request.getMavenHome();
        } else if (System.getProperty("maven.home") != null) {
            mavenHome = new File(System.getProperty("maven.home"));
        }

        if (mavenHome != null && !mavenHome.isDirectory()) {
            File binDir = mavenHome.getParentFile();
            if (binDir != null && "bin".equals(binDir.getName())) {
                // ah, they specified the mvn
                // executable instead...
                mavenHome = binDir.getParentFile();
            }
        }

        if (mavenHome != null && !mavenHome.isDirectory()) {
            throw new IllegalStateException("Maven home is set to: '" + mavenHome + "' which is not a directory");
        }

        logger.debug("Using maven.home of: '" + mavenHome + "'.");
    }

    /**
     * <p>setupMavenExecutable.</p>
     *
     * @param request a Invoker request
     * @throws org.apache.maven.shared.invoker.CommandLineConfigurationException if any.
     */
    protected void setupMavenExecutable(InvocationRequest request) throws CommandLineConfigurationException {
        if (request.getMavenExecutable() != null) {
            mavenExecutable = request.getMavenExecutable();
        }

        if (mavenExecutable == null || !mavenExecutable.isAbsolute()) {
            String executable;
            if (mavenExecutable != null) {
                executable = mavenExecutable.getPath();
            } else {
                executable = "mvn";
            }

            // firs look in project directory
            mavenExecutable = detectMavenExecutablePerOs(baseDirectory, executable);
            if (mavenExecutable == null) {
                // next maven home
                mavenExecutable = detectMavenExecutablePerOs(mavenHome, "/bin/" + executable);
            }

            if (mavenExecutable != null) {
                try {
                    mavenExecutable = mavenExecutable.getCanonicalFile();
                } catch (IOException e) {
                    logger.debug("Failed to canonicalize maven executable: '" + mavenExecutable + "'. Using as-is.", e);
                }
            } else {
                throw new CommandLineConfigurationException("Maven executable: '" + executable + "'"
                        + " not found at project dir: '" + baseDirectory + "' nor maven home: '" + mavenHome + "'");
            }
        }
    }

    private File detectMavenExecutablePerOs(File baseDirectory, String executable) {
        if (Os.isFamily(Os.FAMILY_WINDOWS)) {
            File executableFile = new File(baseDirectory, executable + ".ps1");
            if (executableFile.isFile()) {
                return executableFile;
            }

            executableFile = new File(baseDirectory, executable + ".cmd");
            if (executableFile.isFile()) {
                return executableFile;
            }

            executableFile = new File(baseDirectory, executable + ".bat");
            if (executableFile.isFile()) {
                return executableFile;
            }
        }

        File executableFile = new File(baseDirectory, executable);
        if (executableFile.isFile()) {
            return executableFile;
        }
        return null;
    }

    /**
     * <p>Getter for the field <code>localRepositoryDirectory</code>.</p>
     *
     * @return a {@link java.io.File} object.
     */
    public File getLocalRepositoryDirectory() {
        return localRepositoryDirectory;
    }

    /**
     * <p>Setter for the field <code>localRepositoryDirectory</code>.</p>
     *
     * @param localRepositoryDirectory a {@link java.io.File} object.
     */
    public void setLocalRepositoryDirectory(File localRepositoryDirectory) {
        this.localRepositoryDirectory = localRepositoryDirectory;
    }

    /**
     * <p>Getter for the field <code>logger</code>.</p>
     *
     * @return a {@link org.apache.maven.shared.invoker.InvokerLogger} object.
     */
    public InvokerLogger getLogger() {
        return logger;
    }

    /**
     * <p>Setter for the field <code>logger</code>.</p>
     *
     * @param logger a {@link org.apache.maven.shared.invoker.InvokerLogger} object.
     */
    public void setLogger(InvokerLogger logger) {
        this.logger = logger;
    }

    /**
     * <p>Getter for the field <code>mavenHome</code>.</p>
     *
     * @return a {@link java.io.File} object.
     */
    public File getMavenHome() {
        return mavenHome;
    }

    /**
     * <p>Setter for the field <code>mavenHome</code>.</p>
     *
     * @param mavenHome a {@link java.io.File} object.
     */
    public void setMavenHome(File mavenHome) {
        this.mavenHome = mavenHome;
    }

    /**
     * <p>Getter for the field <code>baseDirectory</code>.</p>
     *
     * @return a {@link java.io.File} object.
     */
    public File getBaseDirectory() {
        return baseDirectory;
    }

    /**
     * <p>Setter for the field <code>baseDirectory</code>.</p>
     *
     * @param baseDirectory a {@link java.io.File} object.
     */
    public void setBaseDirectory(File baseDirectory) {
        this.baseDirectory = baseDirectory;
    }

    /**
     * {@code mavenExecutable} can either be relative to ${maven.home}/bin/ or absolute
     *
     * @param mavenExecutable the executable
     */
    public void setMavenExecutable(File mavenExecutable) {
        this.mavenExecutable = mavenExecutable;
    }

    /**
     * <p>Getter for the field <code>mavenExecutable</code>.</p>
     *
     * @return a {@link java.io.File} object.
     */
    public File getMavenExecutable() {
        return mavenExecutable;
    }
}
