package io.quarkus.maven;

import static io.quarkus.analytics.dto.segment.TrackEventType.DEV_MODE;
import static io.smallrye.common.expression.Expression.Flag.LENIENT_SYNTAX;
import static io.smallrye.common.expression.Expression.Flag.NO_TRIM;
import static java.util.Collections.emptyMap;
import static java.util.function.Predicate.not;
import static org.twdata.maven.mojoexecutor.MojoExecutor.artifactId;
import static org.twdata.maven.mojoexecutor.MojoExecutor.configuration;
import static org.twdata.maven.mojoexecutor.MojoExecutor.executeMojo;
import static org.twdata.maven.mojoexecutor.MojoExecutor.executionEnvironment;
import static org.twdata.maven.mojoexecutor.MojoExecutor.goal;
import static org.twdata.maven.mojoexecutor.MojoExecutor.groupId;
import static org.twdata.maven.mojoexecutor.MojoExecutor.plugin;
import static org.twdata.maven.mojoexecutor.MojoExecutor.version;

import java.io.File;
import java.io.FilterInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Properties;
import java.util.Scanner;
import java.util.Set;
import java.util.function.Consumer;
import java.util.stream.Collectors;

import org.aesh.readline.terminal.impl.ExecPty;
import org.aesh.readline.terminal.impl.Pty;
import org.aesh.terminal.Attributes;
import org.aesh.terminal.utils.ANSI;
import org.apache.commons.lang3.StringUtils;
import org.apache.maven.artifact.Artifact;
import org.apache.maven.artifact.handler.ArtifactHandler;
import org.apache.maven.artifact.handler.manager.ArtifactHandlerManager;
import org.apache.maven.execution.MavenSession;
import org.apache.maven.model.BuildBase;
import org.apache.maven.model.Dependency;
import org.apache.maven.model.DependencyManagement;
import org.apache.maven.model.Plugin;
import org.apache.maven.model.PluginExecution;
import org.apache.maven.model.Profile;
import org.apache.maven.model.Resource;
import org.apache.maven.plugin.AbstractMojo;
import org.apache.maven.plugin.BuildPluginManager;
import org.apache.maven.plugin.MojoExecution;
import org.apache.maven.plugin.MojoExecutionException;
import org.apache.maven.plugin.MojoFailureException;
import org.apache.maven.plugin.PluginParameterExpressionEvaluator;
import org.apache.maven.plugin.descriptor.MojoDescriptor;
import org.apache.maven.plugin.descriptor.PluginDescriptor;
import org.apache.maven.plugin.logging.Log;
import org.apache.maven.plugins.annotations.Component;
import org.apache.maven.plugins.annotations.LifecyclePhase;
import org.apache.maven.plugins.annotations.Mojo;
import org.apache.maven.plugins.annotations.Parameter;
import org.apache.maven.plugins.annotations.ResolutionScope;
import org.apache.maven.project.MavenProject;
import org.apache.maven.shared.utils.cli.CommandLineUtils;
import org.apache.maven.toolchain.Toolchain;
import org.apache.maven.toolchain.ToolchainManager;
import org.codehaus.plexus.component.configurator.expression.ExpressionEvaluationException;
import org.codehaus.plexus.component.configurator.expression.ExpressionEvaluator;
import org.codehaus.plexus.util.xml.Xpp3Dom;
import org.eclipse.aether.RepositorySystem;
import org.eclipse.aether.RepositorySystemSession;
import org.eclipse.aether.artifact.DefaultArtifact;
import org.eclipse.aether.collection.CollectRequest;
import org.eclipse.aether.graph.Exclusion;
import org.eclipse.aether.repository.RemoteRepository;
import org.eclipse.aether.repository.WorkspaceReader;
import org.eclipse.aether.resolution.ArtifactRequest;
import org.eclipse.aether.resolution.ArtifactResolutionException;
import org.eclipse.aether.resolution.ArtifactResult;
import org.eclipse.aether.resolution.DependencyRequest;
import org.eclipse.aether.resolution.DependencyResolutionException;
import org.eclipse.aether.resolution.DependencyResult;
import org.eclipse.aether.util.artifact.JavaScopes;
import org.fusesource.jansi.internal.Kernel32;

import io.quarkus.bootstrap.BootstrapConstants;
import io.quarkus.bootstrap.app.ConfiguredClassLoading;
import io.quarkus.bootstrap.app.QuarkusBootstrap;
import io.quarkus.bootstrap.devmode.DependenciesFilter;
import io.quarkus.bootstrap.model.ApplicationModel;
import io.quarkus.bootstrap.model.PathsCollection;
import io.quarkus.bootstrap.resolver.BootstrapAppModelResolver;
import io.quarkus.bootstrap.resolver.maven.BootstrapMavenContext;
import io.quarkus.bootstrap.resolver.maven.BootstrapMavenContextConfig;
import io.quarkus.bootstrap.resolver.maven.BootstrapMavenException;
import io.quarkus.bootstrap.resolver.maven.MavenArtifactResolver;
import io.quarkus.bootstrap.util.BootstrapUtils;
import io.quarkus.bootstrap.workspace.ArtifactSources;
import io.quarkus.bootstrap.workspace.SourceDir;
import io.quarkus.deployment.dev.DevModeCommandLine;
import io.quarkus.deployment.dev.DevModeCommandLineBuilder;
import io.quarkus.deployment.dev.DevModeContext;
import io.quarkus.deployment.dev.DevModeMain;
import io.quarkus.deployment.dev.ExtensionDevModeJvmOptionFilter;
import io.quarkus.maven.components.CompilerOptions;
import io.quarkus.maven.components.MavenVersionEnforcer;
import io.quarkus.maven.components.QuarkusWorkspaceProvider;
import io.quarkus.maven.dependency.ArtifactCoords;
import io.quarkus.maven.dependency.ArtifactKey;
import io.quarkus.maven.dependency.ResolvedDependency;
import io.quarkus.paths.PathList;
import io.quarkus.runtime.LaunchMode;
import io.smallrye.common.expression.Expression;

/**
 * The dev mojo, that runs a quarkus app in a forked process. A background compilation process is launched and any changes are
 * automatically reflected in your running application.
 * <p>
 * You can use this dev mode in a remote container environment with {@code remote-dev}.
 */
@Mojo(name = "dev", defaultPhase = LifecyclePhase.PREPARE_PACKAGE, requiresDependencyResolution = ResolutionScope.TEST, threadSafe = true)
public class DevMojo extends AbstractMojo {

    private static final Set<String> IGNORED_PHASES = Set.of(
            "pre-clean", "clean", "post-clean");

    private static final List<String> PRE_DEV_MODE_PHASES = List.of(
            "validate",
            "initialize",
            "generate-sources",
            "process-sources",
            "generate-resources",
            "process-resources",
            "compile",
            "process-classes",
            "generate-test-sources",
            "process-test-sources",
            "generate-test-resources",
            "process-test-resources",
            "test-compile");

    private static final String IO_QUARKUS = "io.quarkus";
    private static final String QUARKUS_GENERATE_CODE_GOAL = "generate-code";
    private static final String QUARKUS_GENERATE_CODE_TESTS_GOAL = "generate-code-tests";

    private static final String ORG_APACHE_MAVEN_PLUGINS = "org.apache.maven.plugins";
    private static final String MAVEN_COMPILER_PLUGIN = "maven-compiler-plugin";
    private static final String MAVEN_RESOURCES_PLUGIN = "maven-resources-plugin";
    private static final String MAVEN_SUREFIRE_PLUGIN = "maven-surefire-plugin";
    private static final String MAVEN_TOOLCHAINS_PLUGIN = "maven-toolchains-plugin";

    private static final String ORG_JETBRAINS_KOTLIN = "org.jetbrains.kotlin";
    private static final String KOTLIN_MAVEN_PLUGIN = "kotlin-maven-plugin";

    private static final String BOOTSTRAP_ID = "DevMojo";

    /**
     * The directory for compiled classes.
     */
    @Parameter(readonly = true, required = true, defaultValue = "${project.build.outputDirectory}")
    private File outputDirectory;

    @Parameter(defaultValue = "${project}", readonly = true, required = true)
    protected MavenProject project;

    /**
     * If this server should be started in debug mode. The default is to start in debug mode and listen on
     * port 5005. Whether the JVM is suspended waiting for a debugger to be attached,
     * depends on the value of {@link #suspend}.
     * <p>
     * {@code debug} supports the following options:
     * <table>
     * <tr>
     * <td><b>Value</b></td>
     * <td>Effect</td>
     * </tr>
     * <tr>
     * <td><b>false</b></td>
     * <td>The JVM is not started in debug mode</td>
     * </tr>
     * <tr>
     * <td><b>true</b></td>
     * <td>The JVM is started in debug mode and will be listening on {@code debugHost}:{@code debugPort}</td>
     * </tr>
     * <tr>
     * <td><b>client</b></td>
     * <td>The JVM is started in client mode, and will attempt to connect to {@code debugHost}:{@code debugPort}</td>
     * </tr>
     * <tr>
     * <td><b>{port}</b></td>
     * <td>The JVM is started in debug mode and will be listening on {@code debugHost}:{port}.</td>
     * </tr>
     * </table>
     * By default, {@code debugHost} has the value "localhost", and {@code debugPort} is 5005.
     */
    @Parameter(defaultValue = "${debug}")
    private String debug;

    @Parameter(defaultValue = "${open-lang-package}")
    private boolean openJavaLang;

    /**
     * Allows configuring the modules to add to the application.
     * The listed modules will be added using: {@code --add-modules m1,m2...}.
     */
    @Parameter(defaultValue = "${add-modules}")
    private List<String> modules;

    /**
     * Whether the JVM launch, in debug mode, should be suspended. This parameter is only
     * relevant when the JVM is launched in {@link #debug debug mode}. This parameter supports the
     * following values (all the allowed values are case-insensitive):
     * <table>
     * <th>
     * <td>Value</td>
     * <td>Effect</td>
     * </th>
     * <tr>
     * <td>y or true</td>
     * <td>The debug mode JVM launch is suspended</td>
     * </tr>
     * <tr>
     * <td>n or false</td>
     * <td>The debug mode JVM is started without suspending</td>
     * </tr>
     * </table>
     */
    @Parameter(defaultValue = "${suspend}")
    private String suspend;

    @Parameter(defaultValue = "${debugHost}")
    private String debugHost;

    @Parameter(defaultValue = "${debugPort}")
    private String debugPort;

    @Parameter(defaultValue = "${project.build.directory}")
    private File buildDir;

    @Parameter(defaultValue = "${project.build.sourceDirectory}")
    private File sourceDir;

    @Parameter
    private File workingDir;

    /**
     * Allows configuring arbitrary JVM arguments. Multiple arguments can be specified by delimiting them with a space
     * character.
     */
    @Parameter(defaultValue = "${jvm.args}")
    private String jvmArgs;

    @Parameter(defaultValue = "${quarkus.args}")
    private String argsString;

    @Parameter
    private Map<String, String> environmentVariables = Map.of();

    @Parameter
    private Map<String, String> systemProperties = Map.of();

    /**
     * When enabled, the {@code <environmentVariables>} and {@code <systemPropertyVariables>}
     * elements of the Maven Surefire plugin are copied to environment variables and system
     * properties defined by this plugin. Note that no other Surefire configuration is used
     * (notably {@code <systemProperties>}), only the 2 elements mentioned above.
     * <p>
     * This plugin's {@code <environmentVariables>} and {@code <systemProperties>} have
     * priority, so duplicate keys are not copied.
     * <p>
     * Since environment variables and system properties are global to the entire process,
     * this also affects dev mode (when executed as {@code quarkus:dev}). Because of that,
     * this copying action is disabled by default and requires opt-in.
     */
    @Parameter(defaultValue = "false")
    private boolean copySurefireVariables;

    @Parameter(defaultValue = "${session}")
    private MavenSession session;

    @Parameter(defaultValue = "TRUE")
    private boolean deleteDevJar;

    @Component
    private MavenVersionEnforcer mavenVersionEnforcer;

    @Component
    private ArtifactHandlerManager artifactHandlerManager;

    @Component
    private RepositorySystem repoSystem;

    @Component
    QuarkusWorkspaceProvider workspaceProvider;

    @Parameter(defaultValue = "${repositorySystemSession}", readonly = true)
    private RepositorySystemSession repoSession;

    @Parameter(defaultValue = "${project.remoteProjectRepositories}", readonly = true, required = true)
    private List<RemoteRepository> repos;

    @Parameter(defaultValue = "${project.remotePluginRepositories}", readonly = true, required = true)
    private List<RemoteRepository> pluginRepos;

    /**
     * This value is intended to be set to true when we want to require C2 compilation instead of preventing it from
     * ever kicking in.
     * Setting this will likely have a small negative effect on startup time and should only be done when it absolutely
     * makes sense.
     */
    @Parameter(property = "forceC2")
    private Boolean forceC2;

    /**
     * Whether changes in the projects that appear to be dependencies of the project containing the application to be launched
     * should trigger hot-reload. By default, they do.
     */
    @Parameter(defaultValue = "${noDeps}")
    private boolean noDeps = false;

    /**
     * Optional list of files to watch for changes that trigger a hot reload in dev mode.
     * This is useful for extensions developers that can set this property to their extension's
     * artifacts in their local repository.
     */
    @Parameter(property = "watchedFiles", required = false)
    private List<String> watchedFiles = List.of();

    /**
     * Additional parameters to pass to javac when recompiling changed
     * source files.
     */
    @Parameter
    private List<String> compilerArgs;

    /**
     * Additional compiler arguments
     */
    @Parameter
    private List<CompilerOptions> compilerOptions;

    /**
     * The --release argument to javac.
     */
    @Parameter(defaultValue = "${maven.compiler.release}")
    private String release;

    /**
     * The -source argument to javac.
     */
    @Parameter(defaultValue = "${maven.compiler.source}")
    private String source;

    /**
     * The -target argument to javac.
     */
    @Parameter(defaultValue = "${maven.compiler.target}")
    private String target;

    /**
     * Whether to enforce the quarkus-maven-plugin build goal to be configured.
     * By default, a missing build goal is considered an inconsistency (although the build goal is not <i>required</i>
     * technically).
     * In this case a warning will be logged and the application will not be started.
     */
    @Parameter(defaultValue = "${quarkus.enforceBuildGoal}")
    private boolean enforceBuildGoal = true;

    @Parameter(property = "quarkus.warnIfBuildGoalMissing")
    boolean warnIfBuildGoalMissing = true;

    @Component
    private WorkspaceReader wsReader;

    @Component
    private BuildPluginManager pluginManager;

    @Component
    private ToolchainManager toolchainManager;

    private Map<ArtifactKey, Plugin> pluginMap;

    @Component
    protected QuarkusBootstrapProvider bootstrapProvider;

    @Parameter(defaultValue = "${mojoExecution}", readonly = true, required = true)
    MojoExecution mojoExecution;

    @Component
    BuildAnalyticsProvider analyticsProvider;

    /**
     * A comma-separated list of Maven plugin keys in {@code groupId:artifactId} format
     * (for example {@code org.codehaus.mojo:flatten-maven-plugin} and/or goal prefixes,
     * (for example {@code flatten}) that should be skipped when {@code quarkus:dev} identifies
     * Maven plugin goals that should be executed before the application is launched in dev mode.
     * <p>
     * Only the {@code flatten} Maven plugin is skipped by default.
     */
    @Parameter(defaultValue = "org.codehaus.mojo:flatten-maven-plugin")
    Set<String> skipPlugins;

    /**
     * Extension dev mode JVM option filter configuration.
     * <p>
     * Allows disabling all JVM options configured by extensions, for example
     *
     * <pre>{@code
     *     <extensionJvmOptions>
     *         <!-- disable JVM options from all the extensions -->
     *         <disableAll>true</disableAll>
     *     </extensionJvmOptions>*
     * }</pre>
     *
     * or specifying a {@code groupId:artifactId:classifier} artifact pattern
     * to disable options provided by the matching subset of extensions, for example
     *
     * <pre>{@code
     *     <extensionJvmOptions>
     *         <disableFor>
     *             <!-- disable JVM options from all the extensions with groupId org.acme -->
     *             <extension>org.acme</extension>
     *             <!-- disable JVM options configured by io.quarkiverse:quarkus-magic -->
     *             <extension>io.quarkiverse:quarkus-magic</extension>
     *         </disableFor>
     *     </extensionJvmOptions>
     * }</pre>
     */
    @Parameter
    ExtensionDevModeJvmOptionFilter extensionJvmOptions;

    /**
     * Selects given test(s) for continuous testing. This is an alternative to {@code quarkus.test.include-pattern}
     * and {@code quarkus.test.exclude-pattern}; if set, the {@code quarkus.test.[include|exclude]-pattern} configuration
     * is ignored.
     * <p>
     * The format of this configuration property is the same as the Maven Surefire {@code -Dtest=...}
     * <a href="https://maven.apache.org/surefire/maven-surefire-plugin/test-mojo.html#test">format</a>.
     * Specifically: it is a comma ({@code ,}) separated list of globs of class file paths and/or
     * method names. Each glob can potentially be prefixed with an exclamation mark ({@code !}), which makes
     * it an exclusion filter instead of an inclusion filter. Exclusions have higher priority than inclusions.
     * The class file path glob is separated from the method name glob by the hash sign ({@code #}) and multiple
     * method name globs may be present, separated by the plus sign ({@code +}).
     * <p>
     * For example:
     * <ul>
     * <li>{@code Basic*}: all classes starting with {@code Basic}</li>
     * <li>{@code ???Test}: all classes named with 3 arbitrary characters followed by {@code Test}</li>
     * <li>{@code !Unstable*}: all classes except classes starting with {@code Unstable}</li>
     * <li>{@code pkg/**}{@code /Ci*leTest}: all classes in the package {@code pkg} and subpackages, starting
     * with {@code Ci} and ending with {@code leTest}</li>
     * <li>{@code *Test#test*One+testTwo?????}: all classes ending with {@code Test}, and in them, only methods
     * starting with {@code test} and ending with {@code One}, or starting with {@code testTwo} and followed
     * by 5 arbitrary characters</li>
     * <li>{@code #fast*+slowTest}: all classes, and in them, only methods starting with {@code fast} or methods
     * named {@code slowTest}</li>
     * </ul>
     * Note that the syntax {@code %regex[...]} and {@code %ant[...]} is <em>NOT</em> supported.
     */
    @Parameter(property = "test")
    String test;

    /**
     * console attributes, used to restore the console state
     */
    private Attributes attributes;
    private int windowsAttributes;
    private boolean windowsAttributesSet;
    private Pty pty;
    private boolean windowsColorSupport;

    /**
     * Indicates for which launch mode the dependencies should be resolved.
     *
     * @return launch mode for which the dependencies should be resolved
     */
    protected LaunchMode getLaunchModeClasspath() {
        return LaunchMode.DEVELOPMENT;
    }

    @Override
    public void setLog(Log log) {
        super.setLog(log);
        MojoLogger.delegate = log;
    }

    @Override
    public void execute() throws MojoFailureException, MojoExecutionException {

        if (project.getPackaging().equals(ArtifactCoords.TYPE_POM)) {
            getLog().info("Type of the artifact is POM, skipping dev goal");
            return;
        }

        mavenVersionEnforcer.ensureMavenVersion(getLog(), session);

        initToolchain();

        // we always want to compile if needed, so if it is run from the parent it will compile dependent projects
        String bootstrapId = handleAutoCompile();

        if (enforceBuildGoal) {
            final PluginDescriptor pluginDescr = getPluginDescriptor();
            final Plugin pluginDef = getConfiguredPluginOrNull(pluginDescr.getGroupId(), pluginDescr.getArtifactId());
            if (!isGoalConfigured(pluginDef, "build")) {
                if (warnIfBuildGoalMissing) {
                    var currentGoal = getCurrentGoal();
                    getLog().warn(
                            "Skipping " + currentGoal + " as this is assumed to be a support library." +
                                    " To disable this warning set warnIfBuildGoalMissing parameter to false."
                                    + System.lineSeparator() +
                                    "To enable " + currentGoal +
                                    " for this module, make sure the quarkus-maven-plugin configuration includes the build goal"
                                    +
                                    " or disable the enforceBuildGoal flag (via plugin configuration or via" +
                                    " -Dquarkus.enforceBuildGoal=false).");
                }
                return;
            }
        }

        saveTerminalState();

        analyticsProvider.buildAnalyticsUserInput((String prompt) -> {
            System.out.print(prompt);
            try (Scanner scanner = new Scanner(new FilterInputStream(System.in) {
                @Override
                public void close() throws IOException {
                    //don't close System.in!
                }
            })) {
                return scanner.nextLine();
            } catch (Exception e) {
                getLog().debug("Failed to collect user input for analytics", e);
                return "";
            }
        });

        copySurefireVariables();

        try {
            DevModeRunner runner = new DevModeRunner(bootstrapId);
            Map<Path, Long> pomFiles = readPomFileTimestamps(runner);
            Map<Path, Long> additionalWatchedFiles = readAdditionaWatchedFilesTimestampts(watchedFiles);
            runner.run();
            long nextCheck = System.currentTimeMillis() + 100;
            for (;;) {
                //we never suspend after the first run
                suspend = "n";
                long sleep = Math.max(0, nextCheck - System.currentTimeMillis()) + 1;
                Thread.sleep(sleep);
                if (System.currentTimeMillis() > nextCheck) {
                    nextCheck = System.currentTimeMillis() + 100;
                    if (!runner.alive()) {
                        restoreTerminalState();
                        if (!runner.isExpectedExitValue()) {
                            throw new MojoExecutionException("Dev mode process did not complete successfully");
                        }
                        return;
                    }
                    // we need to keep POM files changes separated for reactor distinctions
                    List<String> changedPoms = collectChangeFilesFrom(pomFiles);
                    List<String> changedFiles = collectChangeFilesFrom(additionalWatchedFiles);
                    changedFiles.addAll(changedPoms);
                    if (!changedFiles.isEmpty()) {
                        logChanges(changedFiles);

                        // stop the runner before we build the new one as the debug port being free
                        // is tested when building the runner
                        runner.stop();

                        final DevModeRunner newRunner;
                        try {
                            bootstrapId = handleAutoCompile(changedPoms);
                            newRunner = new DevModeRunner(runner.commandLine.getDebugPort(), bootstrapId);
                        } catch (Exception e) {
                            getLog().info("Could not load changedPoms pom.xml file, changes not applied", e);
                            continue;
                        }
                        newRunner.run();
                        runner = newRunner;
                    }
                }
            }
        } catch (Exception e) {
            throw new MojoFailureException("Failed to run", e);
        }
    }

    private List<String> collectChangeFilesFrom(Map<Path, Long> paths) throws IOException {
        // unless it's a git or some other command, there won't be many files modified in 100 milliseconds
        List<String> changedFiles = new ArrayList<>(1);
        for (Map.Entry<Path, Long> e : paths.entrySet()) {
            long t = Files.getLastModifiedTime(e.getKey()).toMillis();
            if (t > e.getValue()) {
                changedFiles.add(e.getKey().toString());
                paths.put(e.getKey(), t);
            }
        }
        return changedFiles;
    }

    private void logChanges(List<String> changedFiles) {
        final StringBuilder sb = new StringBuilder().append("Restarting dev mode following changes in ");
        sb.append(changedFiles.get(0));
        for (int i = 1; i < changedFiles.size(); ++i) {
            sb.append(", ").append(changedFiles.get(i));
        }
        getLog().info(sb.toString());
    }

    /**
     * if the process is forcibly killed then the terminal may be left in raw mode, which
     * messes everything up. This attempts to fix that by saving the state so it can be restored
     */
    private void saveTerminalState() {
        try {
            if (windowsAttributes > 0) {
                long hConsole = Kernel32.GetStdHandle(Kernel32.STD_INPUT_HANDLE);
                if (hConsole != (long) Kernel32.INVALID_HANDLE_VALUE) {
                    int[] mode = new int[1];
                    windowsAttributes = Kernel32.GetConsoleMode(hConsole, mode) == 0 ? -1 : mode[0];
                    windowsAttributesSet = true;

                    final int VIRTUAL_TERMINAL_PROCESSING = 0x0004; //enable color on the windows console
                    if (Kernel32.SetConsoleMode(hConsole, windowsAttributes | VIRTUAL_TERMINAL_PROCESSING) != 0) {
                        windowsColorSupport = true;
                    }
                }
            }
        } catch (Throwable t) {
            //this only works with a proper PTY based terminal
            //Aesh creates an input pump thread, that will steal
            //input from the dev mode process
            try {
                Pty pty = ExecPty.current();
                attributes = pty.getAttr();
                DevMojo.this.pty = pty;
            } catch (Exception e) {
                getLog().debug("Failed to get a local tty", e);
            }
        }
    }

    private void restoreTerminalState() {
        if (windowsAttributesSet) {
            long hConsole = Kernel32.GetStdHandle(Kernel32.STD_INPUT_HANDLE);
            if (hConsole != (long) Kernel32.INVALID_HANDLE_VALUE) {
                Kernel32.SetConsoleMode(hConsole, windowsAttributes);
            }
        } else {
            if (attributes == null || pty == null) {
                return;
            }
            Pty finalPty = pty;
            try (finalPty) {
                finalPty.setAttr(attributes);
                int height = finalPty.getSize().getHeight();
                String sb = ANSI.MAIN_BUFFER +
                        ANSI.CURSOR_SHOW +
                        "\u001B[0m" +
                        "\033[" + height + ";0H";
                finalPty.getSlaveOutput().write(sb.getBytes(StandardCharsets.UTF_8));
            } catch (IOException e) {
                getLog().error("Error restoring console state", e);
            }
        }
    }

    private String handleAutoCompile() throws MojoExecutionException {
        return handleAutoCompile(List.of());
    }

    /**
     * Invokes Maven project goals that are meant to be executed before quarkus:dev,
     * unless they have already been executed.
     *
     * @param reloadPoms a list of POM files that should be reloaded from disk instead of read from the reactor
     * @return bootstrap id
     * @throws MojoExecutionException in case of an error
     */
    private String handleAutoCompile(List<String> reloadPoms) throws MojoExecutionException {
        List<String> goals = session.getGoals();
        // check for default goal(s) if none were specified explicitly,
        // see also org.apache.maven.lifecycle.internal.DefaultLifecycleTaskSegmentCalculator
        if (goals.isEmpty() && !StringUtils.isEmpty(project.getDefaultGoal())) {
            goals = List.of(StringUtils.split(project.getDefaultGoal()));
        }
        final String currentGoal = getCurrentGoal();

        int latestHandledPhaseIndex = -1;
        for (String goal : goals) {
            if (goal.endsWith(currentGoal)) {
                break;
            }
            if (goal.indexOf(':') >= 0 || IGNORED_PHASES.contains(goal)) {
                continue;
            }
            var i = PRE_DEV_MODE_PHASES.indexOf(goal);
            if (i < 0 || i == PRE_DEV_MODE_PHASES.size() - 1) {
                // all the necessary goals have already been executed
                return null;
            }
            if (i > latestHandledPhaseIndex) {
                latestHandledPhaseIndex = i;
            }
        }

        final String quarkusPluginId = getPluginDescriptor().getId();
        // configured plugin executions by lifecycle phases
        final Map<String, List<PluginExec>> phaseExecutions = new HashMap<>();
        // goals with prefixes on the command line
        final Map<String, Plugin> pluginPrefixes = new HashMap<>();
        String bootstrapId = BOOTSTRAP_ID;
        for (Plugin p : project.getBuildPlugins()) {
            if (p.getExecutions().isEmpty()) {
                continue;
            }
            if (skipPlugins.contains(p.getKey())) {
                if (getLog().isDebugEnabled()) {
                    getLog().debug("Skipping " + p.getId() + " execution according to skipPlugins value");
                }
                continue;
            }
            for (PluginExecution e : p.getExecutions()) {
                if (e.getPhase() != null && !PRE_DEV_MODE_PHASES.contains(e.getPhase())) {
                    // skip executions with phases post quarkus:dev, such as install, deploy, site, etc
                    if (getLog().isDebugEnabled()) {
                        getLog().debug("Skipping " + e.getId() + " of " + p.getId());
                    }
                    continue;
                }
                String goalPrefix = null;
                if (!e.getGoals().isEmpty()) {
                    goalPrefix = getMojoDescriptor(p, e.getGoals().get(0)).getPluginDescriptor().getGoalPrefix();
                    if (skipPlugins.contains(goalPrefix)) {
                        if (getLog().isDebugEnabled()) {
                            getLog().debug("Skipping " + goalPrefix + " execution according to skipPlugins value");
                            continue;
                        }
                        continue;
                    }
                    pluginPrefixes.put(goalPrefix, p);
                    pluginPrefixes.put(p.getId(), p);
                }
                if (e.getPhase() != null) {
                    phaseExecutions.computeIfAbsent(e.getPhase(), k -> new ArrayList<>()).add(new PluginExec(p, goalPrefix, e));
                } else {
                    for (String goal : e.getGoals()) {
                        if (goal.equals(QUARKUS_GENERATE_CODE_GOAL) && p.getId().equals(quarkusPluginId)) {
                            var clone = e.clone();
                            clone.setGoals(List.of(QUARKUS_GENERATE_CODE_GOAL));
                            // this will schedule it before the compile phase goals
                            phaseExecutions.computeIfAbsent("compile", k -> new ArrayList<>())
                                    .add(0, new PluginExec(p, goalPrefix, clone));
                            bootstrapId = e.getId();
                        } else if (goal.equals(QUARKUS_GENERATE_CODE_TESTS_GOAL) && p.getId().equals(quarkusPluginId)) {
                            var clone = e.clone();
                            clone.setGoals(List.of(QUARKUS_GENERATE_CODE_TESTS_GOAL));
                            // this will schedule it before the test-compile phase goals
